diff --git a/.changes/performance-devtools-out-of-production.md b/.changes/performance-devtools-out-of-production.md new file mode 100644 index 000000000..a97b69599 --- /dev/null +++ b/.changes/performance-devtools-out-of-production.md @@ -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. diff --git a/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts b/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts index 0ff7caa34..6de9ecc8c 100644 --- a/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts +++ b/apps/electron-backend-e2e/src/performance/performance-build-config.spec.ts @@ -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(); diff --git a/apps/web/project.json b/apps/web/project.json index 7244c4ac5..c3b017782 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -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" diff --git a/apps/web/src/app/app.config.ts b/apps/web/src/app/app.config.ts index 1fc0d8950..7f307db2d 100644 --- a/apps/web/src/app/app.config.ts +++ b/apps/web/src/app/app.config.ts @@ -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', diff --git a/apps/web/src/environments/store-devtools.providers.dev.ts b/apps/web/src/environments/store-devtools.providers.dev.ts new file mode 100644 index 000000000..1564cf9e7 --- /dev/null +++ b/apps/web/src/environments/store-devtools.providers.dev.ts @@ -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 }), +]; diff --git a/apps/web/src/environments/store-devtools.providers.ts b/apps/web/src/environments/store-devtools.providers.ts new file mode 100644 index 000000000..3925820db --- /dev/null +++ b/apps/web/src/environments/store-devtools.providers.ts @@ -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[] = []; diff --git a/docs/architecture/performance-journeys.md b/docs/architecture/performance-journeys.md index 776e5c3dd..eefbcc2bd 100644 --- a/docs/architecture/performance-journeys.md +++ b/docs/architecture/performance-journeys.md @@ -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..counters`) that the ratchet checker consumes. `--dist ` points the script at another build output, for example the `electron-performance` configuration. diff --git a/tools/performance/journey-baselines.json b/tools/performance/journey-baselines.json index 56c319574..1973664dd 100644 --- a/tools/performance/journey-baselines.json +++ b/tools/performance/journey-baselines.json @@ -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" }