Hacktoberfest 2026:維護者為十月標記出來的 issue,仍然開放、適合新手。 瀏覽 Hacktoberfest issue

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

已關閉
#34,514 0 則留言 2 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 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 內容生成。

描述

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.

主要語言
TypeScript
星號
27k
分支
11.8k
平均合併
1 天 5 小時
30 天內合併 PR
276

環境準備

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

angular/angular-cli 的其他 Issue

查看 angular/angular-cli 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。