From 39f62fa40805c60b2d0dd44bae960ec8fc39764f Mon Sep 17 00:00:00 2001 From: kbrilla Date: Thu, 19 Feb 2026 22:36:06 +0100 Subject: [PATCH] perf(language-service): use lightweight project warmup for Angular analysis avoid per-file semantic diagnostics work when warming up a newly loaded project. add ensureProjectAnalyzed() to the language-service API and use it from the server startup path. implement warmup through public compiler API access with existing perf tracing, and add legacy test coverage for the new warmup flow. --- packages/language-service/api.ts | 8 +++++++ .../language-service/src/language_service.ts | 12 ++++++++++ packages/language-service/src/ts_plugin.ts | 5 +++++ .../test/legacy/ts_plugin_spec.ts | 22 +++++++++++++++++++ .../server/src/session.ts | 14 ++++++++---- 5 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/language-service/api.ts b/packages/language-service/api.ts index d493b6328fd..f8f377713b6 100644 --- a/packages/language-service/api.ts +++ b/packages/language-service/api.ts @@ -104,6 +104,14 @@ export interface LinkedEditingRanges { * whose API surface is a strict superset of TypeScript's language service. */ export interface NgLanguageService extends ts.LanguageService { + /** + * Triggers the Angular compiler's analysis pipeline without performing + * per-file type checking. This is a lighter alternative to calling + * `getSemanticDiagnostics()` when the goal is only to ensure that the + * Angular project has been analyzed (e.g. during project initialization). + */ + ensureProjectAnalyzed(): void; + getTcb(fileName: string, position: number): GetTcbResponse | undefined; /** diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index 80d0c2b7f5f..b06d00bc9ed 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -101,6 +101,18 @@ export class LanguageService { return this.options; } + /** + * Triggers the Angular compiler's analysis pipeline without performing + * per-file type checking. + */ + ensureProjectAnalyzed(): void { + this.withCompilerAndPerfTracing(PerfPhase.LsDiagnostics, (compiler) => { + // Accessing the template type checker forces compiler analysis through + // public API without requiring per-file diagnostics computation. + compiler.getTemplateTypeChecker(); + }); + } + getSemanticDiagnostics(fileName: string): ts.Diagnostic[] { return this.withCompilerAndPerfTracing(PerfPhase.LsDiagnostics, (compiler) => { let diagnostics: ts.Diagnostic[] = []; diff --git a/packages/language-service/src/ts_plugin.ts b/packages/language-service/src/ts_plugin.ts index 5fd6217fb1b..6729856d0ee 100644 --- a/packages/language-service/src/ts_plugin.ts +++ b/packages/language-service/src/ts_plugin.ts @@ -346,8 +346,13 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService { return undefined; } + function ensureProjectAnalyzed(): void { + ngLS.ensureProjectAnalyzed(); + } + return { ...tsLS, + ensureProjectAnalyzed, getSyntacticDiagnostics, getSemanticDiagnostics, getSuggestionDiagnostics, diff --git a/packages/language-service/test/legacy/ts_plugin_spec.ts b/packages/language-service/test/legacy/ts_plugin_spec.ts index ada7fc4f410..6840f4e4927 100644 --- a/packages/language-service/test/legacy/ts_plugin_spec.ts +++ b/packages/language-service/test/legacy/ts_plugin_spec.ts @@ -31,4 +31,26 @@ describe('getExternalFiles()', () => { expect(externalFiles?.length).toBe(3); expect(externalFiles?.[0].endsWith('app.component.ngtypecheck.ts')).toBeTrue(); }); + + it('should return all typecheck files when using ensureProjectAnalyzed', () => { + const {project, tsLS} = setup(); + const plugin = initialize({typescript: ts}); + + let externalFiles = plugin.getExternalFiles?.(project, ts.ProgramUpdateLevel.Full); + expect(externalFiles).toEqual([]); + // Trigger compilation using the lighter ensureProjectAnalyzed() method + // instead of getSemanticDiagnostics(). This initializes the Angular compiler + // (analysis + resolution) without per-file type-checking overhead. + const ngLS = new LanguageService(project, tsLS, {}); + ngLS.ensureProjectAnalyzed(); + // After ensureProjectAnalyzed(), the Angular compiler state is initialized. + // Typecheck files are created lazily during diagnostics, so they don't exist yet. + // But subsequent getSemanticDiagnostics() calls should work correctly since + // the compiler is already analyzed. + ngLS.getSemanticDiagnostics(APP_COMPONENT); + externalFiles = plugin.getExternalFiles?.(project, ts.ProgramUpdateLevel.Full); + // Includes 1 typecheck file, 1 template, and 1 css files + expect(externalFiles?.length).toBe(3); + expect(externalFiles?.[0].endsWith('app.component.ngtypecheck.ts')).toBeTrue(); + }); }); diff --git a/vscode-ng-language-service/server/src/session.ts b/vscode-ng-language-service/server/src/session.ts index 6374e718abd..38673ad2461 100644 --- a/vscode-ng-language-service/server/src/session.ts +++ b/vscode-ng-language-service/server/src/session.ts @@ -292,13 +292,19 @@ export class Session { if (!project.hasRoots()) { return; } - const fileName = project.getRootScriptInfos()[0].fileName; - const label = `Global analysis - getSemanticDiagnostics for ${fileName}`; + const languageService = project.getLanguageService(); + if (!isNgLanguageService(languageService)) { + return; + } + const label = `Global analysis - ensureProjectAnalyzed for ${project.getProjectName()}`; if (isDebugMode) { console.time(label); } - // Getting semantic diagnostics will trigger a global analysis. - project.getLanguageService().getSemanticDiagnostics(fileName); + // Trigger Angular compilation without per-file type checking overhead. + // Previously this used getSemanticDiagnostics() which also ran per-file + // TypeScript type checking on the first root file — wasted work since those + // results were never consumed. + languageService.ensureProjectAnalyzed(); if (isDebugMode) { console.timeEnd(label); }