From 907e22bd37b3ebcfbd9a89b32239bf897a98b420 Mon Sep 17 00:00:00 2001 From: daniel-stoian-lgp Date: Fri, 21 Aug 2026 13:44:16 +0300 Subject: [PATCH] added improvements for webpack --- mixins/unmangled.js | 20 ++++++-- plugins/ILibPlugin/index.js | 99 ++++++++++++++++++++++++++++++++----- 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/mixins/unmangled.js b/mixins/unmangled.js index 2a245a6..d1fafa3 100644 --- a/mixins/unmangled.js +++ b/mixins/unmangled.js @@ -5,11 +5,21 @@ module.exports = { // Allow Terser's optimizations/debug-code-removal but don't minify const terserPlugin = helper.getMinimizerByName(config, 'TerserPlugin'); if (terserPlugin) { - terserPlugin.options.terserOptions = terserPlugin.options.terserOptions || {}; - terserPlugin.options.terserOptions.mangle = false; - terserPlugin.options.terserOptions.output = terserPlugin.options.terserOptions.output || {}; - terserPlugin.options.terserOptions.output.beautify = true; - terserPlugin.options.terserOptions.output.comments = true; + // terser-webpack-plugin resolves `terserOptions` into + // `options.minimizer.options` in its CONSTRUCTOR, and reads only that + // at runtime — `options.terserOptions` does not exist on the instance + // afterwards. This used to assign `terserPlugin.options.terserOptions`, + // which therefore had no effect at all: `--no-minify` silently produced + // fully-minified, fully-mangled output (the only visible difference was + // `output.pathinfo` below). Mutate the resolved options object instead. + const minimizer = terserPlugin.options && terserPlugin.options.minimizer; + const minifyOptions = minimizer && minimizer.options; + if (minifyOptions) { + minifyOptions.mangle = false; + minifyOptions.output = minifyOptions.output || {}; + minifyOptions.output.beautify = true; + minifyOptions.output.comments = true; + } config.output.pathinfo = true; } return config; diff --git a/plugins/ILibPlugin/index.js b/plugins/ILibPlugin/index.js index a82ffbc..20c97c6 100644 --- a/plugins/ILibPlugin/index.js +++ b/plugins/ILibPlugin/index.js @@ -97,6 +97,43 @@ function readManifest(compilation, manifest, opts) { return files; } +// Module-level cache of iLib file contents, keyed by absolute source path. +// The iLib tree is ~70 MB / 6,700 small, effectively-immutable JSON files; +// `shouldEmit` below already lets a `pack` rerun skip re-reading files whose +// on-disk *destination* is current, but that check depends on stat-ing a real +// destination file — `serve`'s output filesystem is in-memory (memfs, via +// webpack-dev-middleware), so there is no destination to stat and every file +// gets read fresh on every single compile of a dev-server session. This cache +// (keyed by the *source* file's mtime/size) makes a repeat compile within the +// same process reuse the already-read Buffer instead of hitting disk again. +const readCache = new Map(); + +// `stat` is the source file's already-computed fs.Stats (from `shouldEmit`, +// which needs it too, to decide whether to emit at all) — passed in rather +// than re-stat-ing here, so each file gets exactly one source stat call, same +// as the original implementation, instead of two. +function cachedReadFile(file, stat, callback) { + // stat is null when shouldEmit's own source stat failed (e.g. a manifest + // entry whose file went missing between the manifest read and now) — skip + // the cache and let fs.readFile below surface the real error, same as the + // original implementation's behavior for a missing source file. + if (stat) { + const cached = readCache.get(file); + if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) { + callback(null, cached.data); + return; + } + } + fs.readFile(file, (err, data) => { + if (err) { + callback(err); + return; + } + if (stat) readCache.set(file, {mtimeMs: stat.mtimeMs, size: stat.size, data}); + callback(null, data); + }); +} + // Read each manifest and process their contents. function handleBundles(compilation, manifests, opts, callback) { if (manifests.length === 0) { @@ -120,41 +157,79 @@ function handleBundles(compilation, manifests, opts, callback) { } } -// Read and emit all the assets in a particular manifest. +// Read and emit all the assets in a particular manifest. The original version +// of this function read one file at a time via a callback-per-file recursion +// (`fs.readFile`, wait, recurse) — for the ~6,700-file iLib tree that +// serializes thousands of disk round-trips into the build's critical path. +// This instead fans reads out over a bounded concurrency pool. +// `pump()`'s while loop advances past skipped files (via `shouldEmit`) +// synchronously with no added recursion depth; only genuine async reads +// increment `pending` and re-enter `pump()` from their callback, so depth +// stays O(1) regardless of file count (unlike a naive recursive pool). function handleManifestFiles(compilation, dir, files, opts, callback) { if (files.length === 0) { callback(); - } else { - const outfile = path.join(dir, files.shift()); - if (shouldEmit(compilation.compiler, outfile, opts.cache)) { - fs.readFile(outfile, (err, data) => { + return; + } + const POOL = 32; + let next = 0; + let pending = 0; + let finished = false; + + function finishIfDone() { + if (!finished && next >= files.length && pending === 0) { + finished = true; + callback(); + } + } + + function pump() { + while (pending < POOL && next < files.length) { + const outfile = path.join(dir, files[next++]); + const decision = shouldEmit(compilation.compiler, outfile, opts.cache); + if (!decision.emit) { + continue; + } + pending++; + cachedReadFile(outfile, decision.srcStat, (err, data) => { if (err) { compilation.errors.push(err); } else { emitAsset(compilation, transformPath(opts.context, outfile), data); } - handleManifestFiles(compilation, dir, files, opts, callback); + pending--; + pump(); }); - } else { - handleManifestFiles(compilation, dir, files, opts, callback); } + finishIfDone(); } + + pump(); } // Determine if the output file exists and if its newer to determine if it should be emitted. +// Returns {emit, srcStat}: srcStat is the source file's fs.Stats (or null if it +// couldn't be stat-ed), returned alongside the decision so handleManifestFiles's +// read doesn't need to stat the same source file a second time. function shouldEmit(compiler, file, cache) { + let srcStat = null; + try { + srcStat = fs.statSync(file); + } catch (e) { + return {emit: true, srcStat: null}; + } if (isNodeOutputFS(compiler)) { try { - const src = fs.statSync(file); const dest = fs.statSync( path.join(compiler.options.output.path, transformPath(compiler.options.context, file)) ); - return src.isDirectory() || src.mtime.getTime() > dest.mtime.getTime() || !cache; + const emit = srcStat.isDirectory() || srcStat.mtime.getTime() > dest.mtime.getTime() || !cache; + return {emit, srcStat}; } catch (e) { - return true; + return {emit: true, srcStat}; } } else { - return true; + return {emit: true, srcStat}; } }