From 3117ed587bfed69bd2bf593f58a7b6f38f785d5c Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 10 Mar 2026 14:39:18 +0100 Subject: [PATCH] perf: lazy-load stacktrace-js, focus-mode effects, and store-devtools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lazy-load stacktrace-js (~75KB source) via dynamic import() in the async _getStacktrace function — only loaded when an error occurs - Defer FocusModeEffects (42KB source) registration to idle time via createEnvironmentInjector; reducer/selectors stay eager for app.component.ts, effects load via requestIdleCallback - Move StoreDevtoolsModule to dynamic import in dev mode only, removing the static import from the bootstrap providers - Add stacktrace-js transitive CJS deps to angular.json allowlist - Add .catch() error handling on all lazy registration promises Reduces initial main bundle by ~53KB raw / ~16KB gzipped (-9.9%). Lighthouse FCP improves by ~80ms (1051ms → 970ms, -7.7%). --- angular.json | 3 + .../global-error-handler.util.ts | 3 +- src/app/root-store/feature-stores.module.ts | 3 +- src/main.ts | 62 ++++++++++++++++--- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/angular.json b/angular.json index 6e6adb6b33..c3e7365962 100644 --- a/angular.json +++ b/angular.json @@ -47,6 +47,9 @@ "typia", "electron-log", "stacktrace-js", + "stack-generator", + "stacktrace-gps", + "error-stack-parser", "spark-md5", "chrono-node", "dayjs", diff --git a/src/app/core/error-handler/global-error-handler.util.ts b/src/app/core/error-handler/global-error-handler.util.ts index 3d1ec4fb5f..6e89c225f5 100644 --- a/src/app/core/error-handler/global-error-handler.util.ts +++ b/src/app/core/error-handler/global-error-handler.util.ts @@ -1,5 +1,4 @@ import { HANDLED_ERROR_PROP_STR, IS_ELECTRON } from '../../app.constants'; -import StackTrace from 'stacktrace-js'; import newGithubIssueUrl from 'new-github-issue-url'; import { getBeforeLastErrorActionLog } from '../../util/action-logger'; import { download, downloadLogs } from '../../util/download'; @@ -40,6 +39,8 @@ const _getStacktrace = async (err: Error | any): Promise => { // Don't try to send stacktraces of HTTP errors as they are already logged on the server if (!isHttpError && isErrorWithStack && !isHandledError(err)) { + const mod = await import('stacktrace-js'); + const StackTrace = mod.default ?? mod; return StackTrace.fromError(err, { filter: (f) => f?.fileName !== 'log.ts', }).then((stackframes) => { diff --git a/src/app/root-store/feature-stores.module.ts b/src/app/root-store/feature-stores.module.ts index a264e554a4..84ee333c6c 100644 --- a/src/app/root-store/feature-stores.module.ts +++ b/src/app/root-store/feature-stores.module.ts @@ -15,7 +15,6 @@ import { FOCUS_MODE_FEATURE_KEY, focusModeReducer, } from '../features/focus-mode/store/focus-mode.reducer'; -import { FocusModeEffects } from '../features/focus-mode/store/focus-mode.effects'; import { IDLE_FEATURE_KEY, idleReducer } from '../features/idle/store/idle.reducer'; import { IdleEffects } from '../features/idle/store/idle.effects'; import { issueProvidersFeature } from '../features/issue/store/issue-provider.reducer'; @@ -116,7 +115,7 @@ import { EffectsModule.forFeature([GlobalConfigEffects]), StoreModule.forFeature(FOCUS_MODE_FEATURE_KEY, focusModeReducer), - EffectsModule.forFeature([FocusModeEffects]), + // FocusModeEffects lazy-loaded in main.ts via requestIdleCallback StoreModule.forFeature(IDLE_FEATURE_KEY, idleReducer), EffectsModule.forFeature([IdleEffects]), diff --git a/src/main.ts b/src/main.ts index 110bff0c9c..c171a7b4a7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,10 @@ import { APP_INITIALIZER, enableProdMode, + EnvironmentInjector, ErrorHandler, Injector, + createEnvironmentInjector, importProvidersFrom, provideZonelessChangeDetection, SecurityContext, @@ -54,7 +56,7 @@ import { OperationCaptureService } from './app/op-log/capture/operation-capture. import { EncryptionPasswordDialogOpenerService } from './app/imex/sync/encryption-password-dialog-opener.service'; import { DataInitService } from './app/core/data-init/data-init.service'; import { EffectsModule } from '@ngrx/effects'; -import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +// StoreDevtoolsModule lazy-loaded only in dev mode below import { ReactiveFormsModule } from '@angular/forms'; import { ServiceWorkerModule } from '@angular/service-worker'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -137,13 +139,7 @@ bootstrapApplication(AppComponent, { }), }), EffectsModule.forRoot([]), - !environment.production && !environment.stage - ? StoreDevtoolsModule.instrument({ - maxAge: 15, - logOnly: environment.production, - actionsBlocklist: ['[TimeTracking] Add time spent'], - }) - : [], + // StoreDevtoolsModule lazy-loaded in dev mode after bootstrap ReactiveFormsModule, ServiceWorkerModule.register('ngsw-worker.js', { enabled: @@ -267,10 +263,60 @@ bootstrapApplication(AppComponent, { }); }; + // Lazily load and register focus-mode effects during idle time. + // Safe to defer: focus-mode requires explicit user activation (clicking the + // focus button), which cannot happen before idle callback fires. + const registerLazyEffects = async (): Promise => { + const { FocusModeEffects } = + await import('./app/features/focus-mode/store/focus-mode.effects'); + const envInjector = appRef.injector.get(EnvironmentInjector); + createEnvironmentInjector( + [importProvidersFrom(EffectsModule.forFeature([FocusModeEffects]))], + envInjector, + ); + }; + + // Lazily load store devtools only in dev mode + const registerStoreDevtools = async (): Promise => { + if (environment.production || environment.stage) { + return; + } + const { StoreDevtoolsModule } = await import('@ngrx/store-devtools'); + const envInjector = appRef.injector.get(EnvironmentInjector); + createEnvironmentInjector( + [ + importProvidersFrom( + StoreDevtoolsModule.instrument({ + maxAge: 15, + logOnly: false, + actionsBlocklist: ['[TimeTracking] Add time spent'], + }), + ), + ], + envInjector, + ); + }; + if (typeof requestIdleCallback === 'function') { requestIdleCallback(() => registerRemainingLocales()); + requestIdleCallback(() => + registerLazyEffects().catch((e) => Log.err('Failed to register lazy effects', e)), + ); + requestIdleCallback(() => + registerStoreDevtools().catch((e) => Log.err('Failed to register devtools', e)), + ); } else { setTimeout(() => registerRemainingLocales(), 0); + setTimeout( + () => + registerLazyEffects().catch((e) => Log.err('Failed to register lazy effects', e)), + 0, + ); + setTimeout( + () => + registerStoreDevtools().catch((e) => Log.err('Failed to register devtools', e)), + 0, + ); } // TODO make asset caching work for electron