Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Server build bundles two copies of Domino, so DOM globals from `init` and the `DominoAdapter` are different classes

Đang mở
#34,514 0 bình luận 2 reaction 0 người được giao Xem trên GitHub

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ả

area: @angular/build
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.mjs additionally re-exports the Domino module it already bundles via init.
  • The main server bundle redirects @angular/platform-server's ../third_party/domino/bundled-domino.mjs import to that export. ./polyfills.server.mjs is already in SERVER_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/init created so DOM globals are available before platform init.
  • angular-cli 1199205bc (v12) — universal schematic adds the import to main.server.ts.
  • angular-cli 979bce45e (v15) — webpack server builder auto-adds it, removing the schematic import. f143171fd gates it on @angular/platform-server being installed (#24188).
  • angular-cli 095f5aba6 / 7defb3635 (v16) — esbuild server bundling imports it in the virtual main.server entry; one copy.
  • angular-cli 886cb3167 (v17) — a separate polyfills.server bundle is created to control zone.js flag ordering for cleaner stack traces. init moves 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 setDomTypes import between init and platform-server is 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

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của angular/angular-cli

Tất cả issue của angular/angular-cli

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.