本文將詳細解析 HTML 模組化 的核心規範、底層運作機制與實務配置要點,協助開發者在專案中快速排查常見問題並落實標準寫法。
在先前的系列文章中,我們已經探討過瀏覽器資源排程與首屏效能的基礎機制:包含在 Core Web Vitals LCP 最大內容繪製優化實作:Sub-parts 四階段拆解、fetchpriority 與首屏載入調校 中剖析的 首屏渲染阻塞延遲(Element Render Delay),以及在 HTML Resource Hints 資源預載入:preload、preconnect、prefetch 與 dns-prefetch 排程機制 中介紹過的 ES Module Map 與 rel="modulepreload" 預編譯管線。
本篇將聚焦於原生網頁元件載入機制的演進:從早期被廢棄的 HTML Imports(<link rel="import">)到現代 HTML Modules 提案與現行標準替代方案——解析為什麼舊版 HTML Imports 會阻塞主執行緒並與 ES Modules 產生依賴衝突,同時提供 WHATWG 宣告式 Shadow DOM(<template shadowrootmode="open">)、TC39 ES2025 Import Attributes(with { type }),以及結合 fetch 原生子資源完整性(SRI integrity)與 DOMParser 快取池的完整解決思路。
常見問題(Q&A)與程式碼範本快速跳轉
如果目前正在將老舊專案中的 <link rel="import"> 遷移至現代標準、排查動態載入 HTML 片段造成的樣式閃爍(FOUC)與 SEO 檢索缺失,或想確認 import ... assert 為何在最新瀏覽器報錯,可以直接點擊下方連結跳轉至對應解答與程式碼範本:
- 常見問題快速跳轉:
- Q1:為什麼舊教材寫的
import ... assert { type: 'json' }在新版 Chrome 與 Node.js 會直接拋出 SyntaxError? - Q2:為什麼在客戶端用
fetch()非同步載入首屏 HTML 片段會傷害 SEO 並引發無樣式內容閃爍(FOUC)?如何透過宣告式 Shadow DOM 解決? - Q3:使用
fetch()載入跨網域或 CDN 上的 HTML 模板時,真的需要自己手寫crypto.subtle.digest才能驗證 SRI 雜湊值嗎? - Q4:為什麼取得遠端 HTML 字串後,建議使用
DOMParser.parseFromString()搭配template複製,而非直接對頁面上已掛載的容器執行innerHTML = html? - Q5:在 WICG HTML Modules 正式成為全瀏覽器原生標準前,Vite 與 Webpack 專案如何優雅地將
.html與.css當作 ES 模組直接匯入?
- Q1:為什麼舊教材寫的
- 可直接套用的生產環境程式碼範本:
如果不趕時間,建議順著下方章節往下讀,接下來會逐一講解相關底層運作原理與實作細節。
一、 為什麼早期 HTML Imports 被瀏覽器廢棄?剖析三大架構瓶頸
1. 早期 link rel="import" 的運作方式與三大致命瓶頸
在 2013 年 Web Components v0 草案時期,Google 曾提出 HTML Imports(<link rel="import" href="card.html">),企圖讓開發者像引入外部樣式表一樣,在 <head> 中直接引入包含 <template>、<style> 與 <script> 的獨立 HTML 子文件:
<!-- 早期已廢棄的 Web Components v0 寫法(Chrome 80 已徹底移除支援) -->
<head>
<link rel="import" href="/components/user-card.html">
</head>
<body>
<script>
// 陷阱:依賴全域 link.import 屬性存取子文件 DOM,且阻塞後方腳本執行
const importDoc = document.querySelector('link[rel="import"]').import;
</script>
</body>
這項看似直覺的設計,在實際大規模應用與標準化審查中暴露出三個嚴重的架構缺陷,Mozilla 明確表態不實作、Safari 也從未支援,最終於 Chrome 80 正式從 Blink 引擎移除,W3C 也已將 HTML Imports 草案列為 Discontinued Draft:
1. 獨立子文件解析與內部同步腳本阻塞主文件渲染:當主頁面解析器遇到 <link rel="import"> 時,瀏覽器必須為每個被引入的 .html 建立獨立的 Document 實例,並依序下載、解析且同步執行子文件內部的 <script>,同時還會阻塞主文件中後續的腳本與樣式表評估。若元件之間存在多層巢狀引入,就會形成嚴重的網路瀑布流(Network Waterfall),大幅拖慢首屏 LCP 最大內容繪製時間。
圖:舊版 link rel="import" 會建立獨立的子 Document 並執行內部同步腳本,導致主文件的渲染管線被迫排隊等待
2. 脫離 ES Modules 模組依賴圖(Module Graph),引發重複載入與全域汙染:在 ES6(ES2015)正式確立 import / export 作為 JavaScript 唯一官方模組標準後,HTML Imports 卻維持著自己獨立的「HTML 網址去重池」,無法與瀏覽器的 ES Module Map 互通。這導致在 HTML Import 內部載入的腳本預設運行於全域作用域(Global Scope),不僅容易汙染 window 命名空間,當同一個工具函式同時被 HTML Import 與 <script type="module"> 引用時,也無法透過單一模組圖進行依賴去重與打包樹搖(Tree-shaking)。
圖:HTML Imports 與 ES Modules 各自維護互不相通的載入管線,造成全域命名空間汙染與模組重複下載
3. 搜尋引擎爬蟲不檢索且純客戶端替換易引發 FOUC 與 CLS:Googlebot 以常青版 Chromium 算繪頁面,而 Chromium 自 Chrome 80 起已不認得 <link rel="import">,透過它引入的外部文件內容自然不會出現在算繪結果中;而當開發者退而求其次,改在頁面載入後以未經封裝的 fetch() 將 HTML 與 <style> 塞入首屏空白容器時,不僅關鍵文案無法在第一時間被索引,外部樣式晚於結構抵達更會造成明顯的無樣式內容閃爍(FOUC)與版面位移(CLS)。
圖:將首屏核心內容放在廢棄的 rel="import" 或未預先渲染的客戶端請求中,會導致爬蟲第一波檢索落空並引發畫面閃爍
2. 從 HTML Imports 到 HTML Modules 與現行標準方案對照表
為了徹底解決上述三大瓶頸,W3C 與 WHATWG 將「原生 HTML 模組化」拆解並整合進現有的標準體系中。下表整理了四種技術方案的標準狀態與適用場景:
| 技術方案 | 現行標準狀態 | 模組依賴與執行作用域 | 實務適用場景 |
|---|---|---|---|
1. HTML Imports (rel="import") | 已廢除(Chrome 80+ 移除) | 獨立子文件、腳本汙染全域 | 僅存於舊版 Polymer 1/2 遺留系統,應全面遷移 |
2. 宣告式 Shadow DOM (shadowrootmode) | WHATWG 正式標準(Baseline 2024:Chrome 111、Safari 16.4、Firefox 123 起) | 隨主文件 HTML 串流解析,零 JS 阻塞 | 首屏核心元件與 SSR 架構(兼顧 SEO 檢索與零 FOUC) |
3. fetch() + DOMParser + <template> | 現行原生標準 API 組合 | 運行於標準 ES Module 作用域內 | 非首屏按需動態載入元件(如彈跳視窗、次要面板) |
4. Import Attributes (with { type }) 與 HTML Modules | json 已納入 ES2025;css 由 WHATWG HTML 規範定義(Safari 尚未支援);type: 'html' 仍是 WICG 提案 | 整合進單一 ES Module Graph | 原生匯入 JSON 與 CSSStyleSheet;HTML 可搭配 Vite ?raw 預編譯 |
下圖展示了現代前端架構如何依「首屏 SSR 宣告式渲染」、「非首屏動態 Fetch 快取」與「ES2025 Import Attributes 模組整合」三條路徑取代舊有的 HTML Imports:
圖:現代網頁依場景分流——首屏採用宣告式 Shadow DOM 保障 SEO 與零閃爍,非首屏採用 Fetch + DOMParser 快取,樣式與靜態模板則納入 ES Import Attributes
二、 HTML Modules 提案進展與現代三大替代技術解析
1. TC39 Import Attributes(with { type })與 WICG HTML Modules 語法校準

在閱讀網路上關於 WICG HTML Modules 提案 的早期文章時,經常會看到 import doc from './card.html' assert { type: 'html' } 的寫法。這裡有一個極為重要的標準變更必須注意:
TC39 委員會已將早期的 Import Assertions(assert 關鍵字)正式廢棄,並於 ES2025 標準中全面改為 Import Attributes(with 關鍵字)(Chrome 123+、Safari 17.2+、Firefox 138+ 與 Node.js 20.10+ 皆已實作 with;舊的 assert 只在 Chrome 91~125 與 Node.js 16.14~21.x 出現過,已分別於 Chrome 126 與 Node.js 22 移除,Firefox 與 Safari 則從未支援 assert)。
在當前的標準進度中:
- JSON Modules 與 CSS Module Scripts 已可直接使用
with語法:你可以透過import sheet from './card.css' with { type: 'css' }將外部 CSS 直接匯入為CSSStyleSheet物件,並透過shadowRoot.adoptedStyleSheets = [sheet]在多個元件實例間共用同一份記憶體樣式表。type: 'json'是 ES2025 定義的標準型別;type: 'css'(CSS module scripts)則由 WHATWG HTML 規範定義,目前 Chrome/Edge 123+ 與 Firefox 147+ 支援,Safari 與 Node.js 尚未支援。 - HTML Modules(
with { type: 'html' })仍停留在 WICG 提案:尚未納入 WHATWG HTML 標準,也沒有瀏覽器正式出貨(Chrome Platform Status 標示為「In development」)。其設計目標是讓 ES Modules 能直接匯入解析好的Document或HTMLTemplateElement;在瀏覽器全面開放原生type: 'html'之前,實務上可透過打包工具(如 Vite 的import html from './card.html?raw')或標準的fetch+DOMParser管線達成相同效果。
2. 結合原生 SRI(RequestInit.integrity)、Promise 快取池與 DOMParser 的動態載入架構
當我們需要在瀏覽器端非同步載入遠端 HTML 片段時,為避免重複發起網路請求或遭遇跨站內容竄改,建議採用以下四階段安全管線(註:關於 Shadow DOM 內部的樣式隔離細節與 Custom Elements 四大生命週期 Hook,我們將在後續的專文中分別深入探討,本節專注於模組載入器本身):
- 以
Map<string, Promise<HTMLTemplateElement>>快取進行中的請求:當頁面上同時有 10 個元件實例請求同一個/templates/card.html時,直接快取 Promise 本身(而非等回應完成後才快取結果),確保瀏覽器只會發起唯一一次 HTTP 請求。 - 直接使用
fetch(url, { integrity })原生子資源完整性驗證:根據 WHATWG Fetch 規範,fetch()的第二個參數RequestInit原生內建integrity欄位(例如sha384-...)。當遠端 CDN 回傳的 HTML 位元組雜湊值不符時,瀏覽器會在底層直接拒絕該回應並拋出TypeError,完全不需要自己手寫crypto.subtle.digest計算雜湊。 - 透過
DOMParser.parseFromString(html, 'text/html')離線解析並存入<template>:使用DOMParser在離線文件(Inert Document)中完成語法樹建構,不會執行內部的<script>,也不會觸發主頁面的排版重算;接著將節點移入離線的<template>元素,後續每個元件只需呼叫.content.cloneNode(true)即可高速複製出獨立的 DOM 子樹。
圖:透過 Promise 快取池合併並發請求,並結合 fetch 原生 integrity 驗證與 DOMParser 離線解析,打造高效能的 HTML 模板載入管線
三、 可直接套用的現代 HTML 模組化與宣告式載入程式碼範本
範本一:具備 Promise 去重快取池、原生 SRI integrity 驗證與 DOMParser 的 HTML 模板載入器 {#example-secure-html-loader}
以下範本提供一套可直接用於生產環境的原生 ES Module 模板載入器(htmlModuleLoader.js),完整解決「多實例並發重複請求」、「CDN 子資源完整性(SRI)驗證」與「離線 <template> 高速複製」三大需求:
// htmlModuleLoader.js
/**
* 儲存 URL 對應之 Promise<HTMLTemplateElement>,防止多組件並發時重複發起網路請求
*/
const templatePromiseCache = new Map();
/**
* 安全載入並解析遠端 HTML 模組為可重複複製的 HTMLTemplateElement
* @param {string} url 遠端 HTML 模板路徑
* @param {Object} [options]
* @param {string} [options.integrity] 選填之原生 SRI 雜湊值 (例如 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC')
* @returns {Promise<HTMLTemplateElement>}
*/
export function importHtmlTemplate(url, { integrity } = {}) {
if (templatePromiseCache.has(url)) {
return templatePromiseCache.get(url);
}
const loadPromise = (async () => {
const fetchOptions = {
method: 'GET',
mode: 'cors',
credentials: 'same-origin',
};
// 直接使用 WHATWG Fetch 原生支援的 integrity 屬性交由瀏覽器底層驗證 SRI
if (integrity) {
fetchOptions.integrity = integrity;
}
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTML 模組載入失敗 (${response.status}): ${url}`);
}
const rawHtml = await response.text();
// 使用 DOMParser 於離線文件中安全解析 HTML(解析期間不執行內部腳本、不觸發主頁面 Reflow)
const parser = new DOMParser();
const doc = parser.parseFromString(rawHtml, 'text/html');
// 若遠端檔案只有一個頂層 <template>,直接取出;否則包裝為新的 HTMLTemplateElement
// 注意:檔案開頭的 <template> 會被 HTML Parser 放進 head 而非 body,因此 head 與 body 要一起判斷
const topLevelElements = [...doc.head.children, ...doc.body.children];
if (topLevelElements.length === 1 && topLevelElements[0].localName === 'template') {
return topLevelElements[0];
}
const template = document.createElement('template');
// 將 head 內的 <style> 與 body 內的結構一併移入離線 template.content
template.content.append(
...doc.head.querySelectorAll('style, link[rel="stylesheet"]'),
...doc.body.childNodes
);
return template;
})().catch((err) => {
// 若載入失敗則自快取移除,允許後續重試
templatePromiseCache.delete(url);
throw err;
});
templatePromiseCache.set(url, loadPromise);
return loadPromise;
}
/**
* 將遠端 HTML 模組實例化並掛載至目標容器(支援 Shadow DOM 隔離)
*/
export async function mountHtmlModule(hostElement, url, { integrity, useShadow = true } = {}) {
const template = await importHtmlTemplate(url, { integrity });
const clonedFragment = template.content.cloneNode(true);
const targetRoot = useShadow
? hostElement.shadowRoot || hostElement.attachShadow({ mode: 'open' })
: hostElement;
targetRoot.replaceChildren(clonedFragment);
return targetRoot;
}
範本二:支援 SSR 首屏零閃爍的宣告式 Shadow DOM(shadowrootmode="open")與 CSS Module Scripts 整合範例 {#example-declarative-shadow-dom-css-module}
針對需要被搜尋引擎完整索引、且絕不能出現首屏樣式閃爍(FOUC)的核心區塊,建議直接採用 WHATWG 宣告式 Shadow DOM(Declarative Shadow DOM) 搭配 Import Attributes with { type: 'css' } 樣式表模組(CSS module scripts,Safari 尚未支援,需相容 Safari 時可改用 <link rel="stylesheet"> 或內嵌 <style>):
<!-- 1. 伺服器端或靜態 HTML 直接輸出 shadowrootmode="open"(自訂標籤依 WHATWG 規範採用全小寫 kebab-case) -->
<!-- 瀏覽器 HTML Parser 解析至此處時,不需任何 JS 即可立即建立隔離的 Shadow Root 並完成首屏繪製 -->
<product-banner-card>
<template shadowrootmode="open">
<style>
:host {
display: block;
padding: 1.25rem;
border-radius: 0.75rem;
background: #f8fafc;
border: 1px solid #cbd5e1;
}
.banner-title {
margin: 0 0 0.5rem;
font-size: 1.25rem;
color: #0f172a;
}
</style>
<article>
<h2 class="banner-title">
<slot name="title">原生宣告式組件標題</slot>
</h2>
<p><slot name="summary">此區塊於初始 HTML 解析階段即完成渲染,爬蟲可直接檢索且零 FOUC 閃爍。</slot></p>
</article>
</template>
<!-- 外部投射之語意化內容(直接存在於主文件 Light DOM,爬蟲不需執行 JS 就能讀到) -->
<span slot="title">次世代雲端儲存方案</span>
<span slot="summary">支援跨區即時同步與硬體級加密保護。</span>
</product-banner-card>
// 2. 客戶端進階整合:使用 Import Attributes (with { type: 'css' }) 匯入 CSS module script,共用構造式樣式表
import sharedCardSheet from './shared-card.css' with { type: 'css' };
export function enhanceCardWithSharedStyles(hostElement) {
const shadowRoot = hostElement.shadowRoot || hostElement.attachShadow({ mode: 'open' });
// 將匯入的 CSSStyleSheet 直接附加至 adoptedStyleSheets,多個組件共用同一份記憶體樣式
shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, sharedCardSheet];
}
四、 開發者常見問題(Q&A)與排查注意事項
Q1:為什麼舊教材寫的 import ... assert { type: 'json' } 在新版 Chrome 與 Node.js 會直接拋出 SyntaxError? {#qa-1}
原因說明:assert 是 TC39 早期在 Stage 3 階段實驗性推出的關鍵字(Import Assertions)。後來標準委員會為了允許屬性影響模組載入行為(而不僅僅是被動斷言),決定將關鍵字正式更名為 with(Import Attributes) 並納入 ES2025 標準。自 Chrome 126 與 Node.js 22 起,V8 引擎已正式移除對 assert 關鍵字的支援;Firefox 與 Safari 則從未支援 assert。以 Node.js v22 執行舊寫法會得到:
SyntaxError: Unexpected identifier 'assert'
解法:請將專案中所有的 assert { type: 'json' } 或 assert { type: 'css' } 全面替換為標準語法 with { type: 'json' } 與 with { type: 'css' }。
Q2:為什麼在客戶端用 fetch() 非同步載入首屏 HTML 片段會傷害 SEO 並引發無樣式內容閃爍(FOUC)?如何透過宣告式 Shadow DOM 解決? {#qa-2}
原因說明:當首屏的主視覺或文章導覽列依賴客戶端 JavaScript 執行 fetch('/components/hero.html') 後才注入 DOM 時,使用者會先看到空白畫面或版面劇烈跳動(CLS);同時若局部 HTML 內含外部 <link rel="stylesheet">,在樣式表下載完成前會先露出未排版的原始文字(FOUC)。
解法:
- 首屏關鍵區塊採用宣告式 Shadow DOM(
<template shadowrootmode="open">):直接在伺服器端輸出的初始 HTML 中內嵌<template shadowrootmode="open">,瀏覽器 HTML 解析器在讀取串流時就會原生建立 Shadow Root 並套用內部<style>,不需等待 JS 執行、避免 FOUC,內容也直接存在於初始 HTML 中供爬蟲讀取。 - 非首屏動態載入元件:在替換內容前,先等待內部樣式資源就緒,或搭配
adoptedStyleSheets同步掛載已預先匯入的CSSStyleSheet。
Q3:使用 fetch() 載入跨網域或 CDN 上的 HTML 模板時,真的需要自己手寫 crypto.subtle.digest 才能驗證 SRI 雜湊值嗎? {#qa-3}
原因說明:不需要!許多開發者誤以為只有 <script integrity="..."> 與 <link integrity="..."> 標籤才支援子資源完整性(Subresource Integrity, SRI),因而自己用 TextEncoder 與 crypto.subtle.digest('SHA-256', ...) 手動算雜湊。事實上根據 WHATWG Fetch 規範與 MDN RequestInit 文件,原生 fetch() 早就內建了 integrity 參數支援。
解法:在呼叫 fetch(url, { integrity: 'sha384-...' }) 時直接傳入標準 SRI 雜湊字串即可。瀏覽器會在取得回應內容後自動比對雜湊,一旦不符便以網路錯誤處理,fetch() 的 Promise 會以 TypeError 拒絕,同時請確保跨網域 CDN 已正確回應 Access-Control-Allow-Origin CORS 標頭。
Q4:為什麼取得遠端 HTML 字串後,建議使用 DOMParser.parseFromString() 搭配 template 複製,而非直接對頁面上已掛載的容器執行 innerHTML = html? {#qa-4}
原因說明:如果頁面上有 20 個卡片元件,每次都對已掛載在畫面上的 DOM 容器直接執行 container.innerHTML = htmlString,瀏覽器就必須把同一段 HTML 字串重複進行 20 次詞法分析(Tokenization)與語法樹建構(關於 HTML Parser 建構 DOM 樹的成本,請參閱 DOM 樹狀結構與渲染引擎揭秘)。
解法:如本文範本一所示,首次取得字串時使用 new DOMParser().parseFromString(rawHtml, 'text/html') 解析一次並轉存至離線的 HTMLTemplateElement 快取中;後續第 2~20 個元件只需呼叫 template.content.cloneNode(true) 直接複製已解析好的節點,不必再重新對 HTML 字串做詞法分析。
Q5:在 WICG HTML Modules 正式成為全瀏覽器原生標準前,Vite 與 Webpack 專案如何優雅地將 .html 與 .css 當作 ES 模組直接匯入? {#qa-5}
原因說明:在現代前端工程化環境中,我們通常希望在建置階段(Build Time)就把小型 HTML 模板打包進模組圖中,省去執行期的額外 HTTP 往返。
解法:
- 在 Vite 專案中:依據 Vite 官方靜態資源規範,只要在匯入路徑後方加上
?raw後綴(例如import cardHtml from './card.html?raw'),Vite 就會在編譯階段將 HTML 檔案內嵌為字串模組;樣式則可使用?inline取得處理後的 CSS 字串(不會自動注入頁面),再以new CSSStyleSheet()搭配replaceSync()轉成可放進adoptedStyleSheets的樣式表。 - 在 Webpack 專案中:可依 Webpack Asset Modules 文件,在
module.rules中將.html配置為 Webpack 5 內建的type: 'asset/source'(無需額外安裝舊版raw-loader),即可直接透過import cardHtml from './card.html'取得模板字串。
五、 總結
回顧從 <link rel="import"> 到 HTML Modules 與現代標準替代方案的演進,前端架構已全面收斂至 ES Modules 單一模組圖與宣告式 HTML 標準:
- 徹底淘汰
<link rel="import">與過時的assert語法:改用 ES2025 標準的with { type: 'json' | 'css' }(Import Attributes) 與標準 ES Module 載入管線。 - 依首屏與非首屏區分載入策略:首屏核心與 SEO 敏感元件優先採用 宣告式 Shadow DOM(
<template shadowrootmode="open">);非首屏互動元件則採用fetch(url, { integrity })+Promise快取池 +DOMParser離線<template>複製。 - 搭配
<link rel="modulepreload">消除模組瀑布流:對於關鍵的 ES 模組與元件載入器,可結合我們在 HTML Resource Hints 資源預載入要點 中介紹的modulepreload提前預載入模組圖。
參考資料與延伸閱讀
- 站內相關效能與結構系列筆記:
Core Web Vitals LCP 最大內容繪製優化實作:Sub-parts 四階段拆解、fetchpriority 與首屏載入調校
HTML Resource Hints 資源預載入:preload、preconnect、prefetch 與 dns-prefetch 排程機制
DOM 樹狀結構與渲染引擎揭秘:Reflow (重排) 與 Repaint (重繪) 的效能優化全要點 - WHATWG、MDN 與 WICG 官方技術規格:
MDN Web Docs — JavaScript Import Attributes (import ... with { type })
web.dev — Declarative Shadow DOM (<template shadowrootmode="open">)
WHATWG HTML Living Standard — Thetemplateelement & Declarative Shadow Roots
MDN Web Docs —DOMParser.parseFromString()method
MDN Web Docs —RequestInit.integrity(Native Fetch Subresource Integrity)
WICG Web Components — HTML Modules Proposal Explainer
Vite Official Guide — Importing Asset as String (?raw)
Webpack — Asset Modules (asset/source)
Chrome Platform Status — HTML Imports(Chrome 80 移除)
Chrome Platform Status — HTML Modules(In development)
W3C — HTML Imports(Discontinued Draft)
Mozilla Hacks — The state of Web Components(Mozilla 對 HTML Imports 的立場)
TC39 — Import Attributes 提案(ES2025 已完成)





發佈留言