diff --git a/extensions/ql-vscode/CHANGELOG.md b/extensions/ql-vscode/CHANGELOG.md
index 5e1c5ac390f..53dba290fc4 100644
--- a/extensions/ql-vscode/CHANGELOG.md
+++ b/extensions/ql-vscode/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 - Add a palette command that allows importing all databases directly inside of a parent folder. [#3797](https://github.com/github/vscode-codeql/pull/3797)
 - Only use VS Code telemetry settings instead of using `codeQL.telemetry.enableTelemetry` [#3853](https://github.com/github/vscode-codeql/pull/3853)
+- Improve the performance of the results view with large numbers of results. [#3862](https://github.com/github/vscode-codeql/pull/3862)
 
 ## 1.16.1 - 6 November 2024
 
diff --git a/extensions/ql-vscode/src/language-support/contextual/cached-operation.ts b/extensions/ql-vscode/src/language-support/contextual/cached-operation.ts
index a50e7d03b01..3c6e845d740 100644
--- a/extensions/ql-vscode/src/language-support/contextual/cached-operation.ts
+++ b/extensions/ql-vscode/src/language-support/contextual/cached-operation.ts
@@ -7,6 +7,7 @@ export class CachedOperation<S extends unknown[], U> {
   private readonly operation: (t: string, ...args: S) => Promise<U>;
   private readonly cached: Map<string, U>;
   private readonly lru: string[];
+  private generation: number;
   private readonly inProgressCallbacks: Map<
     string,
     Array<[(u: U) => void, (reason?: Error) => void]>
@@ -17,6 +18,7 @@ export class CachedOperation<S extends unknown[], U> {
     private cacheSize = 100,
   ) {
     this.operation = operation;
+    this.generation = 0;
     this.lru = [];
     this.inProgressCallbacks = new Map<
       string,
@@ -46,7 +48,7 @@ export class CachedOperation<S extends unknown[], U> {
         inProgressCallback.push([resolve, reject]);
       });
     }
-
+    const origGeneration = this.generation;
     // Otherwise compute the new value, but leave a callback to allow sharing work
     const callbacks: Array<[(u: U) => void, (reason?: Error) => void]> = [];
     this.inProgressCallbacks.set(t, callbacks);
@@ -54,6 +56,11 @@ export class CachedOperation<S extends unknown[], U> {
       const result = await this.operation(t, ...args);
       callbacks.forEach((f) => f[0](result));
       this.inProgressCallbacks.delete(t);
+      if (this.generation !== origGeneration) {
+        // Cache was reset in the meantime so don't trust this
+        // result enough to cache it.
+        return result;
+      }
       if (this.lru.length > this.cacheSize) {
         const toRemove = this.lru.shift()!;
         this.cached.delete(toRemove);
@@ -69,4 +76,11 @@ export class CachedOperation<S extends unknown[], U> {
       this.inProgressCallbacks.delete(t);
     }
   }
+
+  reset() {
+    this.cached.clear();
+    this.lru.length = 0;
+    this.generation++;
+    this.inProgressCallbacks.clear();
+  }
 }
diff --git a/extensions/ql-vscode/src/local-queries/results-view.ts b/extensions/ql-vscode/src/local-queries/results-view.ts
index 18ee93f8543..4e28e9f9c73 100644
--- a/extensions/ql-vscode/src/local-queries/results-view.ts
+++ b/extensions/ql-vscode/src/local-queries/results-view.ts
@@ -75,6 +75,7 @@ import type { App } from "../common/app";
 import type { Disposable } from "../common/disposable-object";
 import type { RawResultSet } from "../common/raw-result-types";
 import type { BqrsResultSetSchema } from "../common/bqrs-cli-types";
+import { CachedOperation } from "../language-support/contextual/cached-operation";
 
 /**
  * results-view.ts
@@ -177,6 +178,8 @@ export class ResultsView extends AbstractWebview<
   // Event listeners that should be disposed of when the view is disposed.
   private disposableEventListeners: Disposable[] = [];
 
+  private schemaCache: CachedOperation<[], BqrsResultSetSchema[]>;
+
   constructor(
     app: App,
     private databaseManager: DatabaseManager,
@@ -206,6 +209,10 @@ export class ResultsView extends AbstractWebview<
         }
       }),
     );
+
+    this.schemaCache = new CachedOperation(
+      this.getResultSetSchemasImpl.bind(this),
+    );
   }
 
   public getCommands(): ResultsViewCommands {
@@ -420,6 +427,7 @@ export class ResultsView extends AbstractWebview<
       );
       return;
     }
+    this.schemaCache.reset();
     // Notify the webview that it should expect new results.
     await this.postMessage({ t: "resultsUpdating" });
     await this._displayedQuery.completedQuery.updateSortState(
@@ -610,6 +618,12 @@ export class ResultsView extends AbstractWebview<
     selectedTable = "",
   ): Promise<BqrsResultSetSchema[]> {
     const resultsPath = completedQuery.getResultsPath(selectedTable);
+    return this.schemaCache.get(resultsPath);
+  }
+
+  private async getResultSetSchemasImpl(
+    resultsPath: string,
+  ): Promise<BqrsResultSetSchema[]> {
     const schemas = await this.cliServer.bqrsInfo(
       resultsPath,
       PAGE_SIZE.getValue(),
diff --git a/extensions/ql-vscode/src/run-queries-shared.ts b/extensions/ql-vscode/src/run-queries-shared.ts
index 6dbaa8e651b..dac447ee20b 100644
--- a/extensions/ql-vscode/src/run-queries-shared.ts
+++ b/extensions/ql-vscode/src/run-queries-shared.ts
@@ -244,7 +244,7 @@ export class QueryEvaluationInfo extends QueryOutputDir {
    */
   async chooseResultSet(cliServer: CodeQLCliServer) {
     const resultSets = (
-      await cliServer.bqrsInfo(this.resultsPaths.resultsPath, 0)
+      await cliServer.bqrsInfo(this.resultsPaths.resultsPath)
     )["result-sets"];
     if (!resultSets.length) {
       return undefined;