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>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-04 09:46:55 +02:00
1 parent e8b181fcea
commit a87685f6b8
8 files changed
+89 -7

No files matched your search

@@ -0,0 +1,7 @@
---
type: perf
area: performance
---
The app loads slightly less code at startup: a developer-only state
inspection tool is no longer shipped in the desktop and web builds.
@@ -171,6 +171,45 @@ 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',
};
const developmentConfigurations = new Set(['development', 'electron-e2e']);
assert.match(
readFileSync(join(environments, 'store-devtools.providers.ts'), 'utf8'),
/export const storeDevtoolsProviders: EnvironmentProviders\[\] = \[\];/
);
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, [devtoolsReplacement], name);
} else {
assert.doesNotMatch(
JSON.stringify(replacements),
/store-devtools/,
name
);
}
}
});
test('the resolved web build cache output is the renderer directory', () => {
const task = readResolvedWebBuildTask();
+14 -2
View File
@@ -120,7 +120,13 @@
"serviceWorker": false,
"optimization": false,
"extractLicenses": false,
"sourceMap": true
"sourceMap": true,
"fileReplacements": [
{
"replace": "apps/web/src/environments/store-devtools.providers.ts",
"with": "apps/web/src/environments/store-devtools.providers.dev.ts"
}
]
},
"electron-performance": {
"baseHref": "./",
@@ -148,7 +154,13 @@
"serviceWorker": false,
"optimization": false,
"extractLicenses": false,
"sourceMap": true
"sourceMap": true,
"fileReplacements": [
{
"replace": "apps/web/src/environments/store-devtools.providers.ts",
"with": "apps/web/src/environments/store-devtools.providers.dev.ts"
}
]
}
},
"defaultConfiguration": "production"
+2 -3
View File
@@ -21,7 +21,6 @@ import { provideServiceWorker } from '@angular/service-worker';
import { provideEffects } from '@ngrx/effects';
import { provideRouterStore, routerReducer } from '@ngrx/router-store';
import { provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { PlaylistEffects, playlistReducer } from '@iptvnator/m3u-state';
@@ -38,7 +37,7 @@ import {
DataService,
} from '@iptvnator/services';
import { dbConfig } from '@iptvnator/shared/interfaces';
import { AppConfig } from '../environments/environment';
import { storeDevtoolsProviders } from '../environments/store-devtools.providers';
import { routes } from './app.routes';
import { ElectronService } from './services/electron.service';
import { ExternalPlaybackService } from './services/external-playback.service';
@@ -126,7 +125,7 @@ export const appConfig: ApplicationConfig = {
}),
provideEffects([PlaylistEffects]),
provideRouterStore(),
...(AppConfig.production ? [] : [provideStoreDevtools({ maxAge: 25 })]),
...storeDevtoolsProviders,
provideServiceWorker('ngsw-worker.js', {
enabled: shouldEnableServiceWorker(),
registrationStrategy: 'registerWhenStable:30000',
@@ -0,0 +1,8 @@
import { EnvironmentProviders } from '@angular/core';
import { provideStoreDevtools } from '@ngrx/store-devtools';
// Swapped in for store-devtools.providers.ts by the development and
// electron-e2e build configurations only.
export const storeDevtoolsProviders: EnvironmentProviders[] = [
provideStoreDevtools({ maxAge: 25 }),
];
@@ -0,0 +1,7 @@
import { EnvironmentProviders } from '@angular/core';
// Every build ships without the NgRx store devtools. The development and
// electron-e2e configurations replace this file with
// store-devtools.providers.dev.ts, so @ngrx/store-devtools never enters the
// production, PWA or performance bundles.
export const storeDevtoolsProviders: EnvironmentProviders[] = [];
+10
View File
@@ -812,6 +812,16 @@ pnpm --silent run perf:initial-bytes -- --json # machine-readable; --silent ke
node tools/performance/measure-initial-bytes.mjs --summary dist/performance/journey-summary.json
```
Development-only code must stay out of the counter by construction, not by a
runtime flag: `if (!AppConfig.production)` keeps the imported module in
`main.js` because the optimizer does not fold the property read. The NgRx
store devtools therefore come from
`apps/web/src/environments/store-devtools.providers.ts`, an empty list in
every build, which only the `development` and `electron-e2e` configurations
replace with `store-devtools.providers.dev.ts`; a build-config test in
`performance-build-config.spec.ts` keeps it that way. Removing the static
import lowered the counter by 12,565 bytes.
`--summary` writes the journey summary shape (`journeys.<journey>.counters`)
that the ratchet checker consumes. `--dist <dir>` points the script at another
build output, for example the `electron-performance` configuration.
+2 -2
View File
@@ -3,10 +3,10 @@
"journeys": {
"launch": {
"renderer.initialBytes": {
"value": 1605046,
"value": 1596045,
"unit": "bytes",
"slack": 4096,
"updatedAt": "2026-10-01",
"updatedAt": "2026-10-04",
"evidencePr": 1775,
"measuredWith": "pnpm nx build web && pnpm run perf:initial-bytes"
}