JavaScript 動態 import() 失敗怎麼重試?Chrome 155 Beta 的模組載入邊界
使用 JavaScript 程式碼分割時,常見寫法是由互動事件觸發 import('./feature.js')。如果使用者剛好遇到網路中斷、暫時性的 CDN 錯誤或部署切換,第一次載入可能失敗;在部分瀏覽器行為中,失敗結果會被快取,第二次呼叫 import() 只會立即失敗,重新連線也無法補救。
Chrome for Developers 在 2026 年 9 月 16 日的 Chrome 155 Beta 公告說明,模組載入失敗後可以手動重試,例如再次呼叫 import()。這是 Beta channel 的瀏覽器行為更新,不是可以假設所有瀏覽器都已支援的新 JavaScript API;產品仍應自己控制重試次數、錯誤 UI 和 fallback。
先確認失敗真的值得重試
重試適合處理「可能短暫恢復」的失敗,不適合掩蓋部署錯誤。可以先把原因分成三類:
| 類型 | 例子 | 建議 |
|---|---|---|
| 暫時性網路問題 | 連線中斷、行動網路切換、CDN 短暫逾時 | 延遲後有限重試 |
| 版本切換問題 | HTML 是新版本,但 chunk 仍來自舊快取 | 重新取得入口或顯示重新整理提示 |
| 確定性錯誤 | 404、語法錯誤、CSP 或權限阻擋 | 優先修部署或設定,不要無限重試 |
import() 的 rejection 通常不會提供一個足以準確分類所有原因的跨瀏覽器錯誤型別。要判斷是否值得重試,應搭配網路請求紀錄、CDN/伺服器狀態與前端錯誤監控,而不是看到 TypeError 就一律再試。
用 wrapper 控制次數與等待時間
可以把載入函式包起來,讓每個 feature 自己保留相同的重試規則:
function wait(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds));}
async function importWithRetry(load, { attempts = 2, delayMs = 250, shouldRetry = () => false,} = {}) { if (!Number.isInteger(attempts) || attempts < 1 || attempts > 3) { throw new RangeError('attempts must be an integer from 1 to 3'); } if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > 30_000) { throw new RangeError('delayMs must be between 0 and 30000'); }
for (let attempt = 1; attempt <= attempts; attempt += 1) { try { return await load(); } catch (error) { if (attempt === attempts || !shouldRetry(error, attempt)) throw error; await wait(delayMs); } }}attempts = 2 是初始載入加一次重試;最多 3 次與 30 秒延遲上限是此 wrapper 的策略,不是瀏覽器規格。shouldRetry 必須是同步回傳 boolean 的判斷函式,預設為 false;不要傳入 async 函式,因為 Promise 本身是 truthy,無法當作判斷結果。
重試按鈕不能用 once 代表「成功一次」
addEventListener(..., { once: true }) 在第一次事件後移除 listener,不會等待非同步載入成功。第一次操作耗盡重試仍失敗時,按鈕即使留在畫面上也無法再操作。改成用狀態鎖防連點,載入失敗時保留入口;成功初始化之後才封住重複啟動。
下面的 helper 只管理一次 feature 的生命週期。它把載入與初始化分開捕捉:載入失敗可再次操作,初始化失敗則停下,因為 start() 可能已掛上部分 listener 或改動 DOM,不能直接當成可重試的網路錯誤。
function createFeatureLauncher(load, start) { let busy = false; let finished = null;
return async function launch() { if (finished) return finished; if (busy) return { status: 'busy' }; busy = true; try { let feature; try { feature = await load(); } catch (error) { return { status: 'load-failed', error }; } try { await start(feature); finished = { status: 'ready' }; } catch (error) { finished = { status: 'start-failed', error }; } return finished; } finally { busy = false; } };}接到 UI 時,讓一個 launcher 對應一個按鈕,listener 只綁定一次。以下是產品整合示意,需自行提供按鈕、錯誤提示與 feature.js:
const launch = createFeatureLauncher( () => importWithRetry(() => import('./feature.js'), { shouldRetry: () => true, // 使用者確認連線恢復後,容許一次額外嘗試。 }), (feature) => feature.start(),);needsRetry.addEventListener('click', async () => { needsRetry.disabled = true; const result = await launch(); try { if (result.status === 'load-failed' || result.status === 'start-failed') { showLoadError(result.error); // 產品自行提供可見錯誤與恢復步驟。 } } finally { needsRetry.disabled = result.status !== 'load-failed'; }});每次使用者操作最多嘗試載入兩次,這是單次上限,並非整個頁面的全域上限。正式 UI 可另加累計次數、冷卻時間或重新整理入口。shouldRetry: () => true 也不能識別 404 或 CSP;錯誤來源仍需搭配部署與網路紀錄。
start-failed 後應提示重新整理或由元件提供可驗證的清理流程,不自動再呼叫 start。這個 helper 沒有卸載或取消 import 的能力;會切換路由的元件,還要設計離開後不再初始化的生命週期保護。
載入失敗和執行失敗要分開
MDN 的 import() 說明 區分 loading、linking 與 evaluation。模組頂層程式拋錯屬於 evaluation failure,失敗結果會被快取;再 import 同一個模組不會替你修好程式。Chrome 公告的重試改善針對 module load,不能推論成所有 rejection 都會重新執行。
可用以下 Node 測試確認 wrapper 的邊界。把上面的 wait、importWithRetry 與 createFeatureLauncher 宣告放入 retry-check.mjs,接著附上並執行 node retry-check.mjs:
import assert from 'node:assert/strict';
let calls = 0;const result = await importWithRetry(async () => { calls += 1; if (calls === 1) throw new Error('temporary'); return 'loaded';}, { delayMs: 0, shouldRetry: () => true });assert.equal(result, 'loaded');assert.equal(calls, 2);
calls = 0;const failure = new Error('stop');await assert.rejects(importWithRetry(async () => { calls += 1; throw failure;}), (error) => error === failure);assert.equal(calls, 1);await assert.rejects(importWithRetry(async () => 'unused', { attempts: 0,}), RangeError);// 載入失敗後仍可再操作;pending 期間不會建立第二次工作。let resolveLoad;let loads = 0;let starts = 0;const launch = createFeatureLauncher(() => { loads += 1; if (loads === 1) throw new Error('offline'); return new Promise((resolve) => { resolveLoad = resolve; });}, async () => { starts += 1; });assert.equal((await launch()).status, 'load-failed');const pending = launch();assert.equal((await launch()).status, 'busy');assert.equal(loads, 2);resolveLoad({});assert.equal((await pending).status, 'ready');assert.equal((await launch()).status, 'ready');assert.equal(starts, 1);assert.equal(loads, 2);
// start 的部分副作用不能靠重試載入修復。let failedStarts = 0;const broken = createFeatureLauncher(async () => ({}), async () => { failedStarts += 1; throw new Error('partial initialization');});assert.equal((await broken()).status, 'start-failed');assert.equal((await broken()).status, 'start-failed');assert.equal(failedStarts, 1);console.log('retry and launcher checks passed');本次維護流程已在 Node.js v24.13.0 執行上述測試,確認載入失敗可重試、pending 防重入、成功只啟動一次與初始化失敗停下。這只驗證控制流程,不能證明 Chrome 155 的網路行為。瀏覽器驗收仍要用 DevTools 模擬離線後恢復,確認第二次真的出現模組請求;另測頂層拋錯、404 與 CSP 阻擋,記錄是否進入預期 fallback。
Chrome 155 Beta 的改變在哪裡
Chrome 官方描述的問題是:過去失敗的 module load 會被快取,後續重試只會立即失敗;Chrome 155 Beta 讓開發者可以手動重試失敗的模組載入,例如再次呼叫 import()。因此,對可恢復的網路錯誤而言,上面的 wrapper 在新行為下才有機會真的重新取得模組。
這不代表可以把 wrapper 當成瀏覽器支援偵測:
import()本身在不同瀏覽器都可能失敗,但失敗後是否可重新取得模組的行為不一致。- 沒有必要為了「強迫重試」就隨機修改 module specifier;加入 query string 會改變模組識別,可能造成重複載入和 singleton 狀態分裂。
- 如果產品不能鎖定 Chrome Beta,請把 retry 視為漸進式改善,並保留重新整理、離線提示或預先打包的 fallback。
若你的載入流程同時包含 API 請求,請將模組載入重試和請求 timeout 分開設計;可參考 AbortSignal.timeout() 的請求逾時處理,不要把一次 API 失敗和 chunk 載入失敗混成同一個計數器。
版本切換時不要只靠重試
前端部署常見的另一種錯誤是:使用者手上的 HTML 指向新 chunk,但 service worker、瀏覽器或 CDN 還保留舊版本。這時可以搭配版本策略處理:
- 在 HTML 或執行期保留目前前端版本識別,收到 chunk 載入錯誤時記錄它。
- 第一次重試前先確認網路請求是否真的恢復,不要在短時間內連續打同一個失效 URL。
- 若同一版本連續失敗,顯示「重新整理以取得最新版本」的可操作訊息。
- 只有在確認部署能提供該 chunk 時,才把錯誤歸類成暫時性載入問題。
重新整理不是萬靈丹:如果 CDN 仍在部署或新版本本身缺少檔案,重整只會重新遇到相同錯誤。錯誤事件要包含 chunk URL、前端版本、網路狀態與重試次數,才能與部署紀錄對照。
生產環境的最小檢查表
- 設定全域上限,例如一次初始載入加一次重試,不要讓每個元件無限迴圈。
- 只對可能暫時性的錯誤延遲重試;404、解析錯誤與 policy 阻擋要直接進 fallback。
- 對 retry、最終失敗、重新整理點擊分開記錄,避免只看到「載入失敗」而無法判斷改善效果。
- 在 Chrome Beta 與主要穩定版瀏覽器各測一次,確認舊環境仍能顯示可操作的錯誤訊息。
- 對 lazy-loaded 路由、互動元件與背景預載分別設定策略,不要共用一個不透明的全域重試器。
常見問題
Q: 只要在 import() 外面加 retry,就能支援所有瀏覽器嗎?
A: 不能。Chrome 155 Beta 公告描述的是失敗 module load 的快取行為改變;其他瀏覽器可能仍快取失敗結果。請保留重新整理或替代 UI,並在實際支援矩陣中驗證。
Q: 可以用隨機 query string 強制瀏覽器重新載入 module 嗎?
A: 技術上會改變 module specifier,但不應把它當成預設方案。它可能造成同一模組被載入多份,破壞 singleton 或共用狀態;先修正部署、CDN 與版本策略,再決定是否有明確的 cache-busting 需求。
Q: 遇到 404 的 dynamic import 也要重試嗎?
A: 通常不值得。404 多半表示 chunk 沒有部署、URL 版本不一致或路由設定錯誤,應記錄版本與 URL 並修正部署;無限重試只會延遲錯誤回饋。
參考資料: