Server build bundles two copies of Domino, so DOM globals from `init` and the `DominoAdapter` are different classes
維護者通常 1 天內回覆
已經有一個關聯 PR 被合併了。
- #34527 來自 @alan-agius4 —— 已合併
評估
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 新手友好度
- 55/100
- Issue 類型
- 缺陷
- 描述清晰度
- 描述清楚
- 活躍度
- 活躍
- 技術堆疊
- angular, typescript
- 領域
- build-system
研究方向
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.
由索引模型根據 Issue 內容生成。
描述
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.
- 主要語言
- TypeScript
- 星號
- 27k
- 分支
- 11.8k
- 平均合併
- 1 天 5 小時
- 30 天內合併 PR
- 276
環境準備
- 沒有 Dockerfile 或 Docker Compose 檔案
- 有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
angular/angular-cli 的其他 Issue
-
area: @angular/build gemini-triaged
難度 2/5 1-3 小時 新手友好度 74/100
angular/angular-cli#33955 ·
維護者通常 1 天內回覆
-
area: @angular/cli gemini-triaged
難度 2/5 1-3 小時 新手友好度 72/100
angular/angular-cli#33055 · 1 則留言 · 3 個 reaction ·
維護者通常 1 天內回覆
-
Debug ID changes the bytes of an optimized chunk but not its file name, so cached files fail the SRI check可能已有人在做 關聯的 PR 仍在進行中或已合併。 未關閉area: @angular/build
難度 4/5 3-5 天 新手友好度 55/100
angular/angular-cli#34574 · 2 個 reaction ·
維護者通常 1 天內回覆
-
Angular SSR prerender params allow build-time SSRF, credential leaks, and open redirects可能已有人在做 @SkyZeroZx 於 1 天前認領。 未關閉area: @angular/ssr
難度 4/5 3-5 天 新手友好度 35/100
angular/angular-cli#34569 ·
維護者通常 1 天內回覆
-
Tailwind CSS v4 classes in new or moved files are not picked up by ng serve until another rebuild可能已有人在做 @MeAkib 於 8 天前認領。 未關閉area: @angular/build
難度 4/5 3-5 天 新手友好度 35/100
angular/angular-cli#34446 ·
維護者通常 1 天內回覆
查看 angular/angular-cli 的全部 Issue
相似的 Issue
-
Parallel reticle_verify opens per-flow leases without the source lease's ignoreHTTPSErrors可能已有人在做 @as992949791 今天認領。 未關閉good first issue help wanted
難度 2/5 1-3 小時 新手友好度 85/100
reticlehq/reticle#2033 · 1 則留言 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 78/100
維護者通常 1 天內回覆
-
area:quality p3-medium ready-for-agent type:docs
難度 2/5 1-3 小時 新手友好度 83/100
fderuiter/deruiter.dev#3589 · 1 則留言 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 75/100
manfredsteyer/angular-oauth2-oidc#1514 ·
-
難度 2/5 1-3 小時 新手友好度 76/100
marmelab/react-admin#11431 ·
維護者通常 1 天內回覆