Server build bundles two copies of Domino, so DOM globals from `init` and the `DominoAdapter` are different classes
Los mantenedores suelen responder en 1 día
Ya se ha fusionado un pull request relacionado.
- #34527 de @alan-agius4 — fusionado
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 55/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- angular, typescript
- Área
- build-system
Línea de trabajo
Start with application-code-bundle.ts, especially createServerPolyfillBundleOptions, and trace how the server polyfills and main server bundles resolve @angular/platform-server imports. Then read the application builder behavior specs and add coverage for server output, including the legacy SSR path and dev server bundling. Done means the server output evaluates one Domino copy and the regression cases pass.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Command
build
Is this a regression?
- Yes, this behavior used to work in the previous version
Not a regression. The second copy appeared when the dedicated polyfills.server bundle was introduced in v17 and has been there since.
Description
With outputMode: 'server' (the default ng new --ssr setup), @angular/build:application emits two complete copies of Domino into the server output, and both are evaluated in the same Node process:
| File | Brings in Domino via |
|---|---|
polyfills.server.mjs |
@angular/platform-server/init, pushed unconditionally in createServerPolyfillBundleOptions (application-code-bundle.ts#L184) |
main.server.mjs |
@angular/platform-server itself (DominoAdapter) |
The two bundles are separate esbuild contexts, so the third_party/domino/bundled-domino.mjs module that both entry points import is inlined into each output. main.server.mjs starts with import './polyfills.server.mjs', so both copies load.
The two copies are different class hierarchies
platform-server runs the same shim as init in DominoAdapter.makeCurrent() → setDomTypes() during PLATFORM_INITIALIZER, using its copy. So on the first render every global that init set is overwritten by the second copy. Any code that captured a DOM class at module-evaluation time, which is exactly the scenario init exists to support, then holds a class from the other hierarchy:
// top of some module (e.g. a library doing `class Foo extends HTMLElement`)
const Captured = HTMLElement; // Domino copy #1, from polyfills.server.mjs
// later, during rendering
inject(DOCUMENT).body instanceof Captured; // false (body is from copy #2)
inject(DOCUMENT).body instanceof HTMLElement; // true (global was overwritten)
Captured === HTMLElement; // false
Measured cost of the second copy
Default ng new --ssr app, Angular CLI 22.2.4, @angular/build 22.2.4, @angular/platform-server 22.2.3, Node 26.8.1. "Without" is the same build with @angular/platform-server/init removed from the server polyfills and nothing else changed. Memory is the median over 11 fresh processes per variant, forced GC before each snapshot, after serving 50 SSR requests.
With init (default) |
Without | Cost of the extra copy | |
|---|---|---|---|
polyfills.server.mjs on disk |
235.58 kB | 128 B | 235 kB |
Retained V8 heap after loading main.server.mjs |
3,875 KB | 2,739 KB | ~1.1 MB |
| Retained V8 heap after 50 requests | 12,477 KB | 11,342 KB | ~1.1 MB (does not grow) |
Import time of polyfills.server.mjs |
~7.5 ms | ~0.6 ms | ~7 ms startup |
| Prerendered / SSR HTML | identical | identical | — |
The 1.1 MB is permanent: the ESM module namespace keeps every Domino function and prototype reachable for the lifetime of the process. From a heap-snapshot diff it is roughly 350 KB compiled code and function metadata, 340 KB retained source strings, 200 KB prototype and lookup tables, 110 KB function objects, and V8 internals for the rest. The Object.assign(globalThis, domino.impl) itself is negligible (95 properties, ~7 KB, 0.03 ms); the cost is the second module evaluation.
To be clear about scale: in a fresh default app the duplicate is about 30% of the retained heap after loading the server bundles and about 2% of process RSS once requests are served. Small in absolute terms, but pure overhead on every SSR deployment, and the instanceof inconsistency above comes with it.
Minimal Reproduction
npx @angular/[email protected] new repro --ssr --defaults
cd repro
npx ng build --configuration development # keeps esbuild's module markers
grep -n "^// node_modules/@angular/platform-server" dist/repro/server/*.mjs
Actual:
main.server.mjs:36143:// node_modules/@angular/platform-server/third_party/domino/bundled-domino.mjs
main.server.mjs:52859:// node_modules/@angular/platform-server/fesm2022/_server-chunk.mjs
main.server.mjs:53402:// node_modules/@angular/platform-server/fesm2022/platform-server.mjs
polyfills.server.mjs:4:// node_modules/@angular/platform-server/third_party/domino/bundled-domino.mjs
polyfills.server.mjs:16720:// node_modules/@angular/platform-server/fesm2022/init.mjs
Expected: bundled-domino.mjs appears once across the server output.
Full repro with the memory-measurement harness and the instanceof probe: https://github.com/ChristopherPHolder/ng-double-domino
Exception or Error
(none; the build succeeds)
Your Environment
Angular CLI : 22.2.4
Node.js : 26.8.1
Package Manager : npm 11.19.0
@angular/build 22.2.4
@angular/core 22.2.3
@angular/platform-server 22.2.3
@angular/ssr 22.2.4
Anything else relevant?
Proposed fix (no API change), PR to follow
The polyfills bundle has to stay a separate, externally imported file: it is the only thing that guarantees zone.js and init evaluate before application code. (With code splitting, a side-effect import at the top of the main.server entry is inlined into the entry file while shared chunks are hoisted above it, so moving the init import there runs it after shared app chunks.) So the fix shares the copy rather than moving it:
polyfills.server.mjsadditionally re-exports the Domino module it already bundles viainit.- The main server bundle redirects
@angular/platform-server's../third_party/domino/bundled-domino.mjsimport to that export../polyfills.server.mjsis already inSERVER_GENERATED_EXTERNALS, so this becomes a plain external import.
Verified on the repro above with the patched builder:
| Before | After | |
|---|---|---|
bundled-domino.mjs in main.server.mjs |
yes | no |
main.server.mjs (production) |
537 kB | 301 kB |
Retained heap after loading main.server.mjs |
3,875 KB | 2,831 KB |
body instanceof capturedHTMLElement / captured === HTMLElement |
false / false | true / true |
| SSR response | 200, 22,064 B | 200, 22,064 B |
It covers outputMode: 'server', the legacy ssr + prerender path (same esbuild context, same banner), platform: 'neutral', prerendering, and the dev server when bundling. When packages are external (ng serve prebundling) nothing changes, and Node's module cache already shares one copy there. The change comes with a spec under builders/application/tests/behavior/; today no spec or e2e asserts anything about polyfills.server.mjs.
Related
This fix keeps @angular/platform-server/init in every server build. Whether it should be there at all, and the lack of any user control over the server polyfills list, is a separate request: #34515.
How the duplicate came about
- angular/angular#40559 (v12, 2021) —
@angular/platform-server/initcreated so DOM globals are available before platform init. - angular-cli
1199205bc(v12) —universalschematic adds the import tomain.server.ts. - angular-cli
979bce45e(v15) — webpack server builder auto-adds it, removing the schematic import.f143171fdgates it on@angular/platform-serverbeing installed (#24188). - angular-cli
095f5aba6/7defb3635(v16) — esbuild server bundling imports it in the virtualmain.serverentry; one copy. - angular-cli
886cb3167(v17) — a separatepolyfills.serverbundle is created to control zone.js flag ordering for cleaner stack traces.initmoves into it with the other server polyfills. This is where the second copy starts; the commit is about zone.js and does not mention it. - angular/angular#52342 (v17) — the shared
setDomTypesimport betweeninitandplatform-serveris removed so the CLI can skip the JS transformer on the polyfills bundle (angular-cli#26113). After this the two entry points share no module, which guarantees two copies when bundled separately.
So the duplicate was never a deliberate choice; it fell out of two independent build-performance changes.
- Lenguaje dominante
- TypeScript
- Estrellas
- 27k
- Forks
- 11.8k
- Merge medio
- 1 d 5 h
- PR fusionados (30 d)
- 280
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de angular/angular-cli
-
Can't use an array of hostnames in --allowedHosts cli parameter in @angular/build:dev-serverPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abiertoarea: @angular/build gemini-triaged
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
angular/angular-cli#33955 ·
Los mantenedores suelen responder en 1 día
-
Allow tests to pass without any test files (e.g. via Vitest `passWithNoTests`)Posiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abiertoarea: @angular/cli gemini-triaged
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
angular/angular-cli#33055 · 1 comentario · 3 reacciones ·
Los mantenedores suelen responder en 1 día
-
@angular/build: source-map comment scanner corrupts valid JS containing a regex literalPosiblemente ocupada @alan-agius4 la tomó hoy. Abiertoarea: @angular/build
Dificultad 3/5 1-2 días Aptitud para principiantes 76/100
angular/angular-cli#34590 ·
Los mantenedores suelen responder en 1 día
-
Debug ID changes the bytes of an optimized chunk but not its file name, so cached files fail the SRI checkPosiblemente ocupada @alan-agius4 la tomó hace 1 día. Abiertoarea: @angular/build
Dificultad 4/5 3-5 días Aptitud para principiantes 55/100
angular/angular-cli#34574 · 4 reacciones ·
Los mantenedores suelen responder en 1 día
-
Angular SSR prerender params allow build-time SSRF, credential leaks, and open redirectsPosiblemente ocupada @SkyZeroZx la tomó hace 1 día. Abiertoarea: @angular/ssr
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
angular/angular-cli#34569 ·
Los mantenedores suelen responder en 1 día
Todos los issues de angular/angular-cli
Issues similares
-
b:minor bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
software-mansion/argent#1698 ·
Los mantenedores suelen responder en 1 día
-
e2e-failure ready-to-code
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
redhat-developer/rhdh-plugin-export-overlays#4624 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
better-auth/better-auth#12132 · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
automation missing-model model-sync provider:tinfoil
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
anomalyco/models.dev#12053 ·
Los mantenedores suelen responder en 1 día
-
o8 next debug build aborts on Glass to Dark appearance switch (nil identifier messaged before check)Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
hurttlocker/o8#5112 ·