perf(web): keep the NgRx store devtools out of production bundles (#1810)

* perf(web): keep the NgRx store devtools out of production bundles

app.config.ts imported @ngrx/store-devtools statically and gated it on
AppConfig.production at runtime, so the optimizer kept the module in
main.js for the production, PWA and performance builds. The providers now
come from environments/store-devtools.providers.ts, an empty list in every
build; only the development and electron-e2e configurations swap in the
devtools through fileReplacements. A build-config test keeps it that way.

renderer.initialBytes: 1,608,610 -> 1,596,045 bytes (-12,565) on a local
production build; the baseline is lowered to the measured value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(web): cite #1810 as the initial-bytes baseline evidence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-07 21:19:34 +02:00
1 parent acb8cb0318
commit 8581bddcaf
8 files changed
+109 -8

No files matched your search

@@ -177,6 +177,59 @@ test('only the web performance build installs the tick counter the journeys read
}
});
// @ngrx/store-devtools must not reach the production, PWA or performance
// bundles (renderer.initialBytes): the default providers file is empty and
// only the development configurations swap in the devtools.
test('only development web builds provide the NgRx store devtools', () => {
const environments = join(workspaceRoot, 'apps/web/src/environments');
const devtoolsReplacement = {
replace: 'apps/web/src/environments/store-devtools.providers.ts',
with: 'apps/web/src/environments/store-devtools.providers.dev.ts',
};
// electron-e2e-zoneless is electron-e2e plus the zoneless swap.
const developmentConfigurations = new Set([
'development',
'electron-e2e',
'electron-e2e-zoneless',
]);
assert.match(
readFileSync(join(environments, 'store-devtools.providers.ts'), 'utf8'),
/export const storeDevtoolsProviders: EnvironmentProviders\[\] = \[\];/
);
// The replacement must still provide them, or development builds would
// lose the devtools while this test passed.
assert.match(
readFileSync(
join(environments, 'store-devtools.providers.dev.ts'),
'utf8'
),
/storeDevtoolsProviders: EnvironmentProviders\[\] = \[\s*provideStoreDevtools\(/
);
assert.doesNotMatch(
readFileSync(
join(workspaceRoot, 'apps/web/src/app/app.config.ts'),
'utf8'
),
/@ngrx\/store-devtools/
);
for (const [name, configuration] of Object.entries(
webProject.targets['build'].configurations ?? {}
)) {
const replacements = (configuration['fileReplacements'] ??
[]) as unknown[];
if (developmentConfigurations.has(name)) {
assert.deepEqual(replacements[0], devtoolsReplacement, name);
} else {
assert.doesNotMatch(
JSON.stringify(replacements),
/store-devtools/,
name
);
}
}
});
// Plan item C6 measures zoneless change detection behind a build-time flag:
// each *-zoneless configuration is its base configuration plus one swap of
// the change-detection providers, and nothing else selects that swap.