@@ -23,11 +23,23 @@ import { encodeTranslationToBuffer } from './i18n-translation-encoder';
2323const LOCALIZE_KEYWORD = '$localize' ;
2424
2525/**
26- * The maximum number of locales to process concurrently in a single sliding window.
27- * This caps peak worker memory while maintaining multi-locale batching throughput.
26+ * The baseline number of locales to process concurrently in a single sliding window.
27+ * This caps peak worker memory on low-core machines while maintaining multi-locale batching throughput.
2828 */
2929const DEFAULT_LOCALE_WINDOW_SIZE = 8 ;
3030
31+ /**
32+ * Minimum byte size threshold for a file to be eligible for multi-batch sharding.
33+ * Files below this threshold (< 100 KB) are processed in a single batch to minimize IPC overhead.
34+ */
35+ const SMALL_FILE_FLOOR_BYTES = 100 * 1024 ;
36+
37+ /**
38+ * Ratio of the maximum file size in a window to consider a file "dominant".
39+ * Files within 70% of the largest file are sharded across all workers for maximum concurrency.
40+ */
41+ const DOMINANT_FILE_RATIO = 0.7 ;
42+
3143/**
3244 * Serializes the translation messages for a locale for transfer to an inliner Worker.
3345 *
@@ -248,11 +260,13 @@ export class I18nInliner {
248260 ( name ) => ! name . endsWith ( '.map' ) ,
249261 ) ;
250262
251- // Process locales in sliding windows to cap peak worker memory
252- for ( let i = 0 ; i < localeList . length ; i += DEFAULT_LOCALE_WINDOW_SIZE ) {
253- const windowLocales = localeList . slice ( i , i + DEFAULT_LOCALE_WINDOW_SIZE ) ;
263+ // Process locales in sliding windows to cap peak worker memory.
264+ // Ensure the window has at least enough locales to saturate all available workers on high-core machines.
265+ const windowSize = Math . max ( DEFAULT_LOCALE_WINDOW_SIZE , this . #workerPool. maxThreads || 1 ) ;
266+ for ( let i = 0 ; i < localeList . length ; i += windowSize ) {
267+ const windowLocales = localeList . slice ( i , i + windowSize ) ;
254268 const activeLocales = windowLocales . map ( ( item ) => item . locale ) ;
255- const isLastWindow = i + DEFAULT_LOCALE_WINDOW_SIZE >= localeList . length ;
269+ const isLastWindow = i + windowSize >= localeList . length ;
256270
257271 // Pre-calculate cache key bases and serialized Blobs for each locale in this window
258272 const localeCacheBases = new Map < string , string > ( ) ;
@@ -348,7 +362,6 @@ export class I18nInliner {
348362 if ( uncachedByFile . size > 0 ) {
349363 await this . #processUncachedBatches(
350364 uncachedByFile ,
351- windowLocales . length ,
352365 fileResultsByLocale ,
353366 activeLocales ,
354367 isLastWindow ,
@@ -415,28 +428,53 @@ export class I18nInliner {
415428
416429 async #processUncachedBatches(
417430 uncachedByFile : Map < string , UncachedLocaleEntry [ ] > ,
418- localeCount : number ,
419431 fileResultsByLocale : Map < string , Map < string , TransformedFileResult > > ,
420432 activeLocales ?: string [ ] ,
421433 isLastWindow = true ,
422434 generation ?: number ,
423435 ) : Promise < void > {
424436 const workerCount = this . #workerPool. maxThreads || 1 ;
425- const targetTaskCount = Math . max ( uncachedByFile . size , workerCount * 2 ) ;
426- const localesPerBatch = Math . max (
427- 1 ,
428- Math . ceil ( localeCount / ( targetTaskCount / ( uncachedByFile . size || 1 ) ) ) ,
429- ) ;
430437
431- const workerTasks : Promise < void > [ ] = [ ] ;
432-
433- for ( const [ filename , entries ] of uncachedByFile ) {
438+ // Extract file data and identify the heaviest file size in a single pass
439+ let maxFileSize = 0 ;
440+ const sortedFiles = Array . from ( uncachedByFile , ( [ filename , entries ] ) => {
434441 const codeFile = this . #localizeFiles. get ( filename ) ;
435442 assert ( codeFile !== undefined , 'Localize file must exist: ' + filename ) ;
443+ const fileSize = codeFile . contents . byteLength ;
444+ if ( fileSize > maxFileSize ) {
445+ maxFileSize = fileSize ;
446+ }
447+
448+ return { filename, entries, codeFile, fileSize } ;
449+ } ) ;
450+
451+ // Sort files descending by byte size (Longest Processing Time First / LPT).
452+ // Heavy files (e.g. main.js) are queued first to saturate all worker threads immediately,
453+ // while small files act as gap fillers near the window barrier to prevent tail stragglers.
454+ sortedFiles . sort ( ( a , b ) => b . fileSize - a . fileSize ) ;
455+
456+ const workerTasks : Promise < void > [ ] = [ ] ;
457+
458+ for ( const { filename, entries, codeFile, fileSize } of sortedFiles ) {
436459 const mapFile = this . #localizeFiles. get ( filename + '.map' ) ;
437460 const codeBlob = new Blob ( [ codeFile . contents ] ) ;
438461 const mapBlob = mapFile ? new Blob ( [ mapFile . contents ] ) : undefined ;
439462
463+ let localesPerBatch : number ;
464+ if ( uncachedByFile . size === 1 ) {
465+ // Single file in window: shard across all workers to avoid idle threads
466+ localesPerBatch = Math . max ( 1 , Math . ceil ( entries . length / workerCount ) ) ;
467+ } else if ( fileSize < SMALL_FILE_FLOOR_BYTES ) {
468+ // Small chunks (< 100 KB): process all locales in 1 batch to eliminate IPC overhead
469+ localesPerBatch = entries . length ;
470+ } else if ( fileSize >= maxFileSize * DOMINANT_FILE_RATIO ) {
471+ // Dominant file(s): shard across all workers for maximum multi-core parallelism
472+ localesPerBatch = Math . max ( 1 , Math . ceil ( entries . length / workerCount ) ) ;
473+ } else {
474+ // Intermediate files: moderate sharding
475+ localesPerBatch = Math . max ( 1 , Math . ceil ( entries . length / 2 ) ) ;
476+ }
477+
440478 const ephemeral = isLastWindow && entries . length <= localesPerBatch ;
441479 for ( let i = 0 ; i < entries . length ; i += localesPerBatch ) {
442480 const batchEntries = entries . slice ( i , i + localesPerBatch ) ;
0 commit comments