From b08de815c814ecf2efade7059419401798afd229 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:19:48 -0400 Subject: [PATCH 1/2] refactor(@angular/build): add fast path for untranslated messages in i18n inliner worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When inlining a locale without translations (such as the source locale or when a translation dictionary is undefined), calls to translateMessage() previously invoked ɵtranslate() with an empty dictionary. This caused @angular/localize to throw a MissingTranslationError for every single $localize message site in the application, catching it to retrieve the original message parts from the parsed message. Throwing and catching thousands of Error instances incurs significant V8 stack trace capturing and exception handling overhead. translateMessage() now provides an early fast path when translations is undefined: it directly calls ɵparseMessage(messageParts, substitutions) to parse and strip metadata/placeholder markers without throwing or catching exceptions. --- .../build/src/tools/i18n/i18n-inliner-worker.ts | 14 ++++++++++++-- .../build/src/tools/i18n/i18n-inliner_spec.ts | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts b/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts index 4286f33a1b41..778ca55bb814 100644 --- a/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts +++ b/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts @@ -11,6 +11,7 @@ import { type ɵParsedTranslation, ɵisMissingTranslationError, ɵmakeTemplateObject, + ɵparseMessage, ɵtranslate, } from '@angular/localize'; import { MagicString } from 'magic-string'; @@ -335,11 +336,20 @@ type DiagnosticMessage = { type: 'error' | 'warning'; message: string }; */ function translateMessage( diagnostics: DiagnosticMessage[], - translations: Record, + translations: Record | undefined, messageParts: TemplateStringsArray, substitutions: readonly number[], missingTranslation: 'error' | 'warning' | 'ignore', ): [TemplateStringsArray, readonly number[]] { + // Fast path: untranslated locale (e.g. source locale without a translation dictionary). + // Directly parse the message parts to strip metadata and placeholder markers without + // throwing and catching a MissingTranslationError exception. + if (translations === undefined) { + const message = ɵparseMessage(messageParts, substitutions); + + return [ɵmakeTemplateObject(message.messageParts, message.messageParts), substitutions]; + } + try { return ɵtranslate(translations, messageParts, substitutions) as [ TemplateStringsArray, @@ -512,7 +522,7 @@ async function inlineLocalize( for (const callSite of metadata.callSites) { const [translatedParts, translatedSubstitutions] = translateMessage( diagnostics, - translation || {}, + translation, callSite.messageParts, callSite.expressionIndexes, translation === undefined ? 'ignore' : missingTranslation, diff --git a/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts b/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts index bbc827b94c7f..2fbb6e02ce31 100644 --- a/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts +++ b/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts @@ -136,6 +136,20 @@ describe('I18nInliner', () => { expect(findFile(outputFiles, 'main.js').text).toContain('"Hello"'); }); + it('retains the original messages with placeholders for a locale without translations', async () => { + const source = 'export const greeting = $localize`:@@greeting:Hello ${name}:NAME:!`;\n'; + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', source)], + 'en-US', + undefined, + ); + + expect(errors).toEqual([]); + expect(warnings).toEqual([]); + expect(findFile(outputFiles, 'main.js').text).toContain('`Hello ${name}!`'); + expect(findFile(outputFiles, 'main.js').text).not.toContain('$localize'); + }); + it('warns and retains the original message when a locale is missing a translation', async () => { const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( [browserFile('main.js', GREETING_SOURCE)], From 0ae9022b3613d39845702389ceb211d8151df07a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:38:29 -0400 Subject: [PATCH 2/2] test(@angular/build): add massive-multichunk benchmark scenario Adds a new benchmark scenario modeling a very large application workload: - 800 chunks total (~150 MB JS): 1 main bundle (6 MB) + 250 small chunks (40 KB) + 549 feature chunks (245 KB). - 8 locales, 5,000 translations, sourcemaps ON. This exercises worker thread pool queue capacity, task dispatch throughput, IPC messaging, and memory reclamation across hundreds of files in parallel. --- scripts/benchmarks/i18n/scenarios.mts | 108 ++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/scripts/benchmarks/i18n/scenarios.mts b/scripts/benchmarks/i18n/scenarios.mts index aaebdfd47cf7..5cb16f654d51 100644 --- a/scripts/benchmarks/i18n/scenarios.mts +++ b/scripts/benchmarks/i18n/scenarios.mts @@ -541,6 +541,113 @@ export function createLargeEnterpriseScenario( }; } +/** + * 7. Massive Multichunk Scenario: + * 800 chunks total (~150 MB JS): 1 main bundle (6 MB) + 250 small chunks (40 KB) + 549 feature chunks (245 KB), + * 8 locales, 5,000 translations, sourcemaps ON. + * Stresses worker thread pool queue capacity, dispatch throughput, IPC messaging, and memory reclamation + * across hundreds of parallel files. + */ +export function createMassiveMultichunkScenario( + options: ScenarioFactoryOptions = {}, +): BenchmarkScenario { + let workload: GeneratedWorkload | undefined; + + return { + name: 'massive-multichunk', + description: 'Massive Multichunk: 800 chunks (~150 MB JS), 8 locales, sourcemaps ON', + get inputSizeBytes() { + return workload?.totalInputSizeBytes ?? 0; + }, + get localeCount() { + return DEFAULT_LOCALES_8.length; + }, + async setup() { + await initializeFixtures(); + const files: BuildOutputFile[] = []; + const totalMessages = 5000; + let messageOffset = 0; + + // 1. Dominant entry bundle: 6 MB, 1,000 messages + const mainMessages = 1000; + const { codeFile: mainCode, mapFile: mainMap } = generateSyntheticBundle({ + filename: 'main.js', + targetByteSize: 6 * 1024 * 1024, + messageCount: mainMessages, + withSourceMap: true, + messageIdOffset: messageOffset, + }); + files.push(mainCode); + if (mainMap) { + files.push(mainMap); + } + messageOffset += mainMessages; + + // 2. Small route/dialog chunks (< 100 KB): 250 chunks x 40 KB = 10 MB, 1,000 messages (4 msgs/chunk) + const smallChunkCount = 250; + const smallChunkSizeBytes = 40 * 1024; + const smallChunkMessages = 4; + for (let i = 0; i < smallChunkCount; i++) { + const { codeFile, mapFile } = generateSyntheticBundle({ + filename: `small_chunk_${i}.js`, + targetByteSize: smallChunkSizeBytes, + messageCount: smallChunkMessages, + withSourceMap: true, + messageIdOffset: messageOffset + i * smallChunkMessages, + }); + files.push(codeFile); + if (mapFile) { + files.push(mapFile); + } + } + messageOffset += smallChunkCount * smallChunkMessages; + + // 3. Medium/large feature chunks: 549 chunks x 245 KB = ~134.5 MB, 3,000 messages + const featureChunkCount = 549; + const featureChunkSizeBytes = 245 * 1024; + const remainingMessages = totalMessages - messageOffset; + const messagesPerFeatureChunk = Math.max( + 1, + Math.floor(remainingMessages / featureChunkCount), + ); + for (let i = 0; i < featureChunkCount; i++) { + const { codeFile, mapFile } = generateSyntheticBundle({ + filename: `feature_chunk_${i}.js`, + targetByteSize: featureChunkSizeBytes, + messageCount: messagesPerFeatureChunk, + withSourceMap: true, + messageIdOffset: messageOffset + i * messagesPerFeatureChunk, + }); + files.push(codeFile); + if (mapFile) { + files.push(mapFile); + } + } + + const locales = generateTranslations(DEFAULT_LOCALES_8, totalMessages); + workload = { + files, + locales, + totalInputSizeBytes: calculateInputSizeBytes(files), + }; + }, + async run() { + if (!workload) { + return; + } + const inliner = new I18nInliner({ + missingTranslation: 'warning', + maxConcurrency: options.concurrency, + }); + try { + await inliner.inlineAll(workload.files, workload.locales); + } finally { + await inliner.close(); + } + }, + }; +} + export function getAllScenarios(options: ScenarioFactoryOptions = {}): BenchmarkScenario[] { return [ createStandardAppScenario(options), @@ -549,6 +656,7 @@ export function getAllScenarios(options: ScenarioFactoryOptions = {}): Benchmark createLargeEnterpriseScenario(options), createMonolithicScenario(options), createMultiDominantScenario(options), + createMassiveMultichunkScenario(options), createPersistentCacheWarmScenario(options), ]; }