Using TableSort without doing a query
TableSort has methods to retrieve the sorting order and the field being sorted directly from the query arguments. You can use the values returned by ::getSort() and ::getOrder() to do a custom sorting of your data.
In your render array you can use some (undocumented) options in the #header declaration to control the way the table is initially sorted:

field: the name to use in the query arguments to indicate that the table should be sorted by this column.
sort: one (and only one) of the columns can be given this option (with value either asc or desc). This will be the column that is sorted by default.
initial_click_sort: this can be set to desc if the data should be sorted in descending order if the column header is initially clicked. This defaults to asc but for some data you want to show larger numbers at the top by default.

Here is an example of a controller using data from a custom array:
&lt;?php

declare(strict_types = 1);

namespace Drupal\my_project\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Utility\TableSort;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Demo of a table with sortable columns using data from a custom source.
 */
class SortableTableDemoController extends ControllerBase {

  /**
   * The Symfony request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * Constructs a new SortableTableDemoController.
   *
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
   *   The request stack.
   */
  public function __construct(RequestStack $requestStack) {
    $this-&gt;requestStack = $requestStack;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): self {
    return new static(
      $container-&gt;get('request_stack')
    );
  }

  /**
   * Returns a render array containing data about Linux distributions.
   *
   * @return array
   *   The render array.
   */
  public function linuxDistributionsTable(): array {
    $headers = [
      // The distribution label has the 'sort' option, making it the default.
      [
        'data' =&gt; $this-&gt;t('Distribution'),
        'field' =&gt; 'label',
        'sort' =&gt; 'asc',
      ],
      [
        'data' =&gt; $this-&gt;t('Initial release'),
        'field' =&gt; 'year',
      ],
      // Sorted from highest to lowest on initial click.
      [
        'data' =&gt; $this-&gt;t('Packages'),
        'field' =&gt; 'packages',
        'initial_click_sort' =&gt; 'desc',
      ],
    ];

    $data = [
      ['label' =&gt; 'Arch Linux', 'year' =&gt; 2002, 'packages' =&gt; 77264],
      ['label' =&gt; 'Debian', 'year' =&gt; 1993, 'packages' =&gt; 118326],
      ['label' =&gt; 'Red Hat', 'year' =&gt; 2002, 'packages' =&gt; 12155],
      ['label' =&gt; 'Gentoo Linux', 'year' =&gt; 2002, 'packages' =&gt; 29092],
      ['label' =&gt; 'OpenSUSE', 'year' =&gt; 2006, 'packages' =&gt; 76236],
      ['label' =&gt; 'Ubuntu', 'year' =&gt; 2004, 'packages' =&gt; 89403],
    ];

    // Sort the table according to the options passed in the query arguments.
    $request = $this-&gt;requestStack-&gt;getCurrentRequest();
    $sort = TableSort::getSort($headers, $request);
    $order_by = TableSort::getOrder($headers, $request)['sql'];

    usort($data, function (array $a, array $b) use ($sort, $order_by): int {
      $result = $a[$order_by] &lt;=&gt; $b[$order_by];
      return $sort === 'asc' ? $result : -$result;
    });

    return [
      'table' =&gt; [
        '#type' =&gt; 'table',
        '#header' =&gt; $headers,
        '#rows' =&gt; $data,
      ],
    ];
  }

}