Server build bundles two copies of Domino, so DOM globals from `init` and the `DominoAdapter` are different classes
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 55/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- angular, typescript
- Lĩnh vực
- build-system
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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.
- Ngôn ngữ chính
- TypeScript
- Star
- 27k
- Fork
- 11.8k
- Merge trung bình
- 1 ngày 1 giờ
- Pull request đã merge (30 ngày)
- 223
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của angular/angular-cli
-
Can't use an array of hostnames in --allowedHosts cli parameter in @angular/build:dev-serverĐang mởarea: @angular/build gemini-triaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
angular/angular-cli#33955 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
area: @angular/cli gemini-triaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
angular/angular-cli#33055 · 1 bình luận · 3 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
area: @angular/build
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 55/100
angular/angular-cli#34524 · 2 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
@angular/build@21 depends on [email protected] which is vulnerable to multiple CVEsCó thể đã có người làm @florimond-collette đã nhận 2 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 30/100
angular/angular-cli#34518 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
ssr: string redirectTo drops trailing slash in query and fragmentCó thể đã có người làm @alan-agius4 đã nhận 3 ngày trước. Đang mởarea: @angular/ssr
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 57/100
angular/angular-cli#34511 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của angular/angular-cli
Issue tương tự
-
支持对已取消(cancelled)任务生成报告Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 77/100
Clouditera/VulnHunter#57 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
bug bug:behavior clawsweeper:bulk-filed clawsweeper:fix-shape-clear clawsweeper:no-new-fix-pr clawsweeper:source-repro impact:auth-provider issue-rating: 🦞 diamond lobster P2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
openclaw/openclaw#185297 · 1 bình luận · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
-
bug needs-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
apex-dev-tools/apex-log-parser#199 ·
Maintainer thường phản hồi trong vòng 1 ngày