LoadLazyChunks: Clean up and performance improvements (#4509)

Co-authored-by: V <vendicated@riseup.net>
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
This commit is contained in:
sadan 2026-08-17 19:41:40 -04:00 • committed by GitHub
parent 3e03b47b45
commit d3de91e421
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 43 deletions

View file

@ -14,7 +14,7 @@ import pLimit from "p-limit";
function getWebpackChunkMap() {
const sym = Symbol();
let chunksMap: unknown;
let chunksMap: unknown = null;
Object.defineProperty(Object.prototype, sym, {
get() {
@ -30,9 +30,38 @@ function getWebpackChunkMap() {
return chunksMap as Record<PropertyKey, string> | null;
}
let chunksAlreadyLoaded = false;
export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
if (chunksAlreadyLoaded) {
LazyChunkLoaderLogger.log("Lazy chunks have already been loaded");
return;
}
const queue = pLimit(50);
const workerAssetCache = new Map<string, Promise<boolean>>();
const WORKER_ASSET_REGEX = /importScripts\(|self\.postMessage/;
async function isWorkerAsset(url: string, useQueue: boolean = true): Promise<boolean> {
if (workerAssetCache.has(url)) {
return workerAssetCache.get(url)!;
}
const doFetch = () => {
return fetch(url)
.then(r => r.text())
.then(t => WORKER_ASSET_REGEX.test(t));
};
const res = useQueue
? queue(doFetch)
: doFetch();
workerAssetCache.set(url, res);
return res;
}
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
@ -41,12 +70,12 @@ export async function loadLazyChunks() {
const invalidChunks = new Set<PropertyKey>();
const deferredRequires = new Set<PropertyKey>();
const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers<void>();
const { promise: chunkSearchingDone, resolve: chunkSearchingDoneResolve } = Promise.withResolvers<void>();
// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;
// True if searching promise for that chunk is resolved, false otherwise
let chunkSearchResolvedGetters = [] as Array<() => boolean>;
// This regex loads all language packs which makes webpack finds testing extremely slow, so for now, we prioritize using the one which doesnt include those
// This regex loads all language packs which makes webpack finds testing extremely slow, so for now, we prioritize using the one which doesn't include those
const CompleteLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
const PartialLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
@ -56,7 +85,7 @@ export async function loadLazyChunks() {
// Workaround to avoid loading the CSS debugging chunk which turns the app pink
// const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
// Disabled for now since this causes lots of chunks concatenated into the same module get marked as invalid, and thus not loaded.
// Disabled for now since this causes lots of chunks concatenated into the same module to get marked as invalid, and thus not loaded.
const hasCssDebuggingLoad = foundCssDebuggingLoad = false;
const lazyChunks = factoryCode.matchAll(hasCssDebuggingLoad ? CompleteLazyChunkRegex : PartialLazyChunkRegex);
@ -93,13 +122,7 @@ export async function loadLazyChunks() {
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
const isWorkerAsset = await queue(() =>
fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => /importScripts\(|self\.postMessage/.test(t))
);
if (isWorkerAsset) {
if (await isWorkerAsset(wreq.p + wreq.u(id))) {
invalidChunks.add(id);
invalidChunkGroup = true;
continue;
@ -140,35 +163,27 @@ export async function loadLazyChunks() {
}
}
// setImmediate to only check if all chunks were loaded after this function resolves
// We check if all chunks were loaded every time a factory is loaded
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
// Filter out resolved chunk search promises. If the array length is 0 that means all the pending searches and loadings are done
// and our regex has finished scanning all modules. Once this happens, the artificial "natural" loading of chunks has been completed
// and we can continue with the rest of the code.
// setImmediate here is needed so the filtering also includes the promises created from searching the modules loaded by the current invokation.
// Otherwise, it could resolve before the async code being put at the end of the event loop has a chance to run and add the promise to the array.
// The async code mentioned is the invokation of the current function from the factoryListener.
setTimeout(() => {
let allResolved = true;
for (let i = 0; i < chunksSearchPromises.length; i++) {
const isResolved = chunksSearchPromises[i]();
if (isResolved) {
// Remove finished promises to avoid having to iterate through a huge array everytime
chunksSearchPromises.splice(i--, 1);
} else {
allResolved = false;
}
chunkSearchResolvedGetters = chunkSearchResolvedGetters.filter(isResolvedGetter => !isResolvedGetter());
if (chunkSearchResolvedGetters.length === 0) {
chunkSearchingDoneResolve();
}
if (allResolved) chunksSearchingResolve();
}, 0);
}
function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
let isResolved = false;
searchAndLoadLazyChunks(String(factory))
.then(() => isResolved = true)
.catch(() => isResolved = true);
.finally(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
chunkSearchResolvedGetters.push(() => isResolved);
}
Webpack.factoryListeners.add(factoryListener);
@ -176,7 +191,7 @@ export async function loadLazyChunks() {
factoryListener(wreq.m[moduleId]);
}
await chunksSearchingDone;
await chunkSearchingDone;
Webpack.factoryListeners.delete(factoryListener);
// Require deferred entry points
@ -198,17 +213,16 @@ export async function loadLazyChunks() {
});
await Promise.all(chunksLeft.map(async id => queue(async () => {
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => /importScripts\(|self\.postMessage/.test(t));
// We will deadlock if we use the queue inside a queue func
const isWorkerFile = await isWorkerAsset(wreq.p + wreq.u(id), false);
// Loads the chunk. Currently this only happens with the language packs which are loaded differently
if (!isWorkerAsset) {
if (!isWorkerFile) {
await wreq.e(id);
}
})));
LazyChunkLoaderLogger.log("Finished loading all chunks!");
chunksAlreadyLoaded = true;
} catch (e) {
LazyChunkLoaderLogger.log("A fatal error occurred:", e);
}

View file

@ -17,7 +17,7 @@ async function runReporter() {
try {
ReporterLogger.log("Starting test...");
const { promise: loadLazyChunksDone, resolve: loadLazyChunksResolve } = Promise.withResolvers<void>();
const { promise: loadLazyChunksDone, resolve: loadLazyChunksDoneResolve } = Promise.withResolvers<void>();
// The main patch for starting the reporter chunk loading
addPatch({
@ -28,11 +28,10 @@ async function runReporter() {
}
}, "Vencord Reporter");
// initReporter is called in the patched entry point of Discord
// @ts-expect-error
Vencord.Webpack._initReporter = function () {
// initReporter is called in the patched entry point of Discord
// setImmediate to only start searching for lazy chunks after Discord initialized the app
setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0);
loadLazyChunks().then(loadLazyChunksDoneResolve);
};
await loadLazyChunksDone;