部署 Emscripten 編譯的頁面

Emscripten 編譯的輸出可以直接從命令列在 JS shell 中執行,或託管在網頁上。當以 .html 格式託管 asm.js 和 WebAssembly 編譯的頁面以供瀏覽器執行時,Emscripten 提供了一個預設的HTML shell 檔案,作為啟動程式來執行程式碼,簡化了開發的入門流程。然而,當準備發佈並將內容託管在網站上時,可能需要許多額外的功能和客製化,才能改善訪客體驗。本指南重點介紹了將網站部署到公開場合時應注意的事項。

建置檔案和自訂 Shell

Emscripten 建置輸出包含兩個基本部分:1) 低階編譯程式碼模組和 2) 與其互動的 JavaScript 執行階段。例如,當使用 -o out.html 建置時,編譯的程式碼會儲存在檔案 out.wasm 中,而執行階段則位於檔案 out.js 中。當以 asm.js 為目標時,會有一個額外的二進位檔案 out.mem,其中包含編譯程式碼的靜態記憶體區段。當以 WebAssembly 為目標時,此部分會嵌入在 out.wasm 檔案中。

根據所使用的功能,也可能存在其他建置輸出檔案。如果使用了 Emscripten 檔案封裝器,則會產生二進位 out.data 套件,以及相關聯的 out.data.js 載入器檔案。此外,Emscripten pthreads 和 Fetch API 也有其各自相關的 Web Worker 相關指令碼 .js 輸出檔案。

開發人員可以選擇輸出到 JavaScript 或 HTML。如果輸出 JavaScript (emcc -o out.js),則開發人員應手動建立 out.html 主要頁面,以便在瀏覽器中執行程式碼。當以 HTML 為目標時,使用 emcc -o out.html (建議的建置模式),Emscripten 會自動產生 HTML shell 檔案。可以使用 emcc -o out.html --shell-file path/to/custom_shell.html 連結器指令來自訂此 shell 檔案。將 Emscripten 儲存庫中的預設最小 HTML shell 檔案複製到您的專案樹中,以取得自訂 shell 檔案的良好入門範本。

以下各節提供了改善網站體驗的提示。

最佳化下載大小

頁面快速載入的最大減速通常是需要下載大量專案資產資料,特別是如果頁面大量使用 WebGL 紋理或幾何圖形。編譯程式碼通常比手寫 JavaScript 佔用更多空間,但機器碼壓縮效率很高。因此,當託管 asm.js 和 WebAssembly 時,務必確保所有內容都使用 gzip 壓縮傳輸,現在所有瀏覽器和 CDN 都內建支援此壓縮。與未壓縮的檔案相比,gzip 壓縮 .wasm 檔案平均可減少 60-75% 的大小,因此實際在伺服器上提供未壓縮的檔案沒有任何意義。

  • 若要在 CDN 上提供 gzip 壓縮的資產,請使用 gzip 壓縮工具,並在上傳至 CDN 之前離線預先壓縮資產檔案。有些 Web 伺服器支援即時壓縮檔案,但對於靜態資產內容,應避免這種做法,因為伺服器 CPU 持續重新壓縮檔案的成本可能很高。調整 Web 伺服器的組態,以 HTTP 回應標頭 Content-Encoding: gzip 託管預先壓縮的檔案。這會指示 Web 瀏覽器應在將資料交給頁面本身之前,以透明方式解壓縮下載的內容。

  • 請確保 gzip 壓縮不會混淆資產所提供的 MIME 類型。所有 JavaScript 檔案 (預先壓縮或未壓縮) 最好都使用 HTTP 回應標頭 Content-Type: application/javascript 提供,而所有資產檔案 (.data.mem) 應使用標頭 Content-Type: application/octet-stream 提供。WebAssembly .wasm 檔案應使用 Content-Type: application/wasm 提供。

  • 嘗試將使用 Emscripten --preload-file 連結器標誌預先載入並預先下載的資產資料量降至最低。此資料檔案會在 Emscripten 編譯的應用程式開始執行 main() 函式之前載入,因此儲存在此套件中的所有檔案都會大幅減慢啟動時間。最好將下載的資產檔案分成多個不同的套件,並在 Emscripten 中使用非同步資產下載 API,這些 API 可以在應用程式執行時操作。

  • WebGL 應用程式的資產大小通常取決於紋理的數量,因此使用壓縮紋理格式有助於縮減資產大小。與原生平台相比,Web 可能是一個截然不同的目標平台,因為在 Web 上不一定能假設訪客的硬體會支援特定的壓縮紋理格式,特別是如果開發的網站應同時在行動和桌上型瀏覽器上運作。支援各種硬體的最佳做法是產生多組壓縮紋理,每個支援的平台一組,並根據 WebGL 內容支援的格式下載適當的紋理。

  • 如果目標為多個螢幕尺寸,例如桌上型和行動外觀規格,請考慮將紋理區隔為 SD 和 HD 變體,以加快在顯示解析度較小的行動裝置上的頁面載入速度。

最佳化頁面啟動時間

除了下載頁面之外,啟動順序的其他部分有時也可能很慢。此處要考慮的事項如下:

  • 如果以 asm.js 為目標,並且在 Firefox 或 Edge 上執行,則 Web 頁面主控台會在 asm.js 模組編譯後顯示記錄訊息。此記錄訊息包含有關編譯所花費時間的計時資訊。當 asm.js 指令碼來源檔案新增至 DOM 時,asm.js 編譯會開始,一旦完成,就會呼叫指令碼標籤的 onload 事件。這可用於計時 asm.js 編譯在 Safari、Opera 和 Chrome 上所花費的時間。

  • 建議移轉至 WebAssembly,以加快瀏覽器中編譯程式碼的啟動時間。與 asm.js 相比,WebAssembly 模組的剖析和編譯速度快得多。此外,編譯的 WebAssembly.Module 物件可以手動保存到 IndexedDB,這完全避免了第二次執行的編譯步驟。(請參閱下一節)

  • 有時可能很容易將緩慢啟動錯誤歸因於 asm.js/WebAssembly 編譯,而事實上,速度緩慢的實際原因可能是執行應用程式本身的 main() 函式進入點。這是因為這兩個動作是緊接著執行的。值得精確地分析這兩個動作,請查看 src/preamble.js 中的 function callMain(),它會啟動應用程式 main() 程式碼的執行。如果執行 main() 的時間過長,請考慮將其分成由多個 setTimeout() 呼叫或 emscripten_set_main_loop() 事件迴圈驅動的個別作業。

  • 為了加速網路傳輸,經驗顯示,在一般網路條件下,最快的方法是積極地同時並行啟動所有網路下載(假設只有少數幾個下載),而不是像例如一個接一個地依序下載輸入檔案。因此,為了最大化網路傳輸速度,請嘗試編寫應用程式的主 HTML 頁面,以並行啟動所有需要的網路下載,而不是將它們排隊進行循序傳輸。

  • 如果頁面的第一次載入主要受到網路傳輸的影響,那麼利用 CPU 在等待下載完成時大多處於閒置狀態這一點會很有幫助。這段 CPU 時間可用於執行其他繁重的工作。一個理想的候選方案是在下載其他頁面資源時,同時下載和編譯 asm.js/WebAssembly 模組。

  • 目前在 Windows 系統上已知的一個問題是編譯 WebGL shaders 可能會很慢。這也是一個主要的候選動作,可以在下載頁面的其他資源時並行執行。

提供快速的第二次載入

雖然第一次訪問頁面的體驗可能需要一些時間才能完成所有下載,但可以確保瀏覽器快取第一次訪問的結果,從而使頁面的第二次訪問體驗更快。

  • 所有瀏覽器對於資源都有一個實作定義的限制(20MB 或 50MB),並且大於該限制的檔案將完全繞過瀏覽器的內建網頁快取。因此,建議將大型 .data 檔案由主頁手動快取到 IndexedDB。Emscripten 連結器選項 --use-preload-cache 可用於讓 Emscripten 實現此功能,儘管可能希望在 HTML 頁面上以自訂方式手動管理此操作,因為這樣可以控制將資源快取到哪個資料庫,以及將使用哪種方案從其中移除資料。

  • 雖然 .wasm 檔案會像任何資源一樣自動快取,但它們仍然必須在瀏覽器中編譯後才能實例化。幸運的是,基於 Chromium 的瀏覽器支援自動快取編譯後的 WebAssembly 模組(請閱讀此 v8.dev 部落格文章)。以前,建議透過 IndexedDB 手動快取編譯後的 WebAssembly 模組;但是,這現在很大程度上不受支援(詳情請參閱此 WebAssembly 規格票)。

  • 如果編譯後的 C/C++ 程式碼本身執行任何計算,例如在 main() 中,可以在第二次載入時跳過,請使用 IndexedDB 或 localStorage API 在頁面執行之間快取此計算的結果。IndexedDB 適用於儲存大型檔案,但它是以非同步方式運作。另一方面,localStorage API 是完全同步的,但其使用僅限於儲存小型 Cookie 樣式的資料欄位。

  • 在實作基於 IndexedDB 的快取時,最好注意,作為一個執行磁碟存取的非同步 API,IndexedDB 操作也有一些延遲。因此,如果在啟動時執行多個讀取操作,最好在可能的情況下並行啟動所有操作,以減少延遲。

  • 另一個持久化資料的重點是,為了對使用者提供最佳實務,當使用 IndexedDB 或 localStorage 來持久化大量資料時,最好提供明確的視覺識別,並提供一個簡單的機制來清除或解除安裝該資料。這是因為目前瀏覽器沒有實作方便的 UI 來細緻刪除這些儲存中的資料,但清除資料通常以「清除所有頁面的快取」類型的選項呈現。

為編譯程式碼保留記憶體

asm.js 和 WebAssembly 應用程式的一個固有特性是它們需要一個線性的記憶體區塊來表示應用程式的 堆積。這通常是 Emscripten 編譯頁面執行的最大單一記憶體配置,因此,如果使用者的系統記憶體不足,則它是最大的失敗風險。

由於此記憶體配置需要是連續的,因此可能會發生使用者的瀏覽器程序確實有足夠的記憶體,但只有該程序的位址空間過於分散,並且沒有足夠的線性位址空間可供滿足配置。為避免此問題,最佳實務是在主頁的頂部,在執行任何其他配置或頁面腳本載入動作之前,預先配置 WebAssembly.Memory 物件(asm.js 的 ArrayBuffer)。這可確保配置有最大的成功機會。請參閱欄位 Module['buffer']Module['wasmMemory'] 以取得更多資訊。

此外,可以選擇為需要此類大型配置的網頁特定啟用內容程序隔離。若要利用此機制,請在提供主 HTML 頁面時,指定 HTTP 回應標頭 Large-Allocation: <MBytes>。此支援目前已在 Firefox 53 中實作。

最後,很容易在頁面載入後,意外地緊抓著不需要的大型記憶體區塊。例如,在 WebAssembly 中,一旦 WebAssembly 模組已實例化為 WebAssembly.Instance 物件,就不再需要記憶體中的原始 WebAssembly.Module 物件,最好清除對其的所有參考,以便垃圾回收器可以回收它,因為模組物件可能會有數十 MB 的大小。同樣,請確保所有 XHR 檔案、資源資料和大型腳本在不再使用時不再被參考。查看瀏覽器的記憶體分析工具,以及 Firefox 中的 about:memory 頁面,以執行記憶體分析,確保記憶體沒有被浪費。

穩健的錯誤處理

為了提供最佳的使用者體驗,請確保考慮到頁面可能失敗的不同方式,並向使用者提供良好的錯誤報告。特別是,請按照以下最佳實務的檢查清單進行操作。

  • 目標是盡早失敗。使用者感到沮喪的一個很大原因來自於使用者的系統尚未準備好執行給定的頁面,但錯誤只有在等待一分鐘下載 100MB 的資源後才會顯現出來的情況。例如,嘗試在實際載入頁面之前預先配置所需的堆積記憶體。這樣,如果記憶體配置失敗,則會立即失敗,而無需嘗試任何資源下載。

  • 如果已知不支援特定的瀏覽器,請盡可能抵制讀取 navigator.userAgent 欄位以阻擋使用該瀏覽器的使用者的誘惑。例如,如果您的頁面需要 WebGL 2 但已知 Safari 不支援它,請不要使用以下類型的檢查排除 Safari 使用者

    if (navigator.userAgent.indexOf('Safari') != -1) alert('Your browser does not support WebGL 2!');
    

而是偵測實際錯誤

if (!canvas.getContext('webgl2')) alert('Your browser does not support WebGL 2!'); // And look for webglcontextcreationerror here for an error reason.

這樣,一旦稍後提供對特定功能的支持,該頁面將具有未來相容性。

  • 透過模擬不同的問題和瀏覽器限制,預先測試各種失敗案例。例如,在 Firefox 上,可以透過導覽至 about:config 並將偏好設定 webgl.enable-webgl2 設定為 false 來手動停用 WebGL 2。這可讓您偵錯您的頁面在這種情況下將向使用者呈現哪種類型的錯誤報告。若要為了測試目的而完全停用 WebGL 支援,請將偏好設定 webgl.disabled 設定為 true

  • 在使用 IndexedDB 時,請準備好處理當使用者即將用完可用磁碟空間或允許的網域配額時的配額不足錯誤。

  • 如果使用任何預先載入的檔案包,請為 WebAssembly.Memory 物件和預先載入的檔案包配置不切實際的記憶體量,以模擬記憶體不足錯誤。請確保記憶體不足錯誤已正確標記為此類(並向使用者或錯誤資料庫報告)。

  • 透過以程式設計方式中止 XHR 下載、實際中斷網路存取或使用 Fiddler 等外部工具,來模擬下載逾時。這些類型的工具可以顯示出許多意外的失敗案例,並協助診斷此類情況的錯誤處理路徑是否符合期望。

  • 使用網路限制器工具來限制下載或上傳頻寬速度,以模擬緩慢的網路連線。這可能會發現與網路傳輸的時序相依性相關的錯誤。例如,可能會隱式地假設一個小型網路傳輸會在大型網路傳輸之前完成,但情況可能並非總是如此。

  • 在本地開發頁面時,請使用本地網頁伺服器執行測試,而不僅僅是透過 file:// URL。Emscripten 原始碼樹中的腳本 emrun.py 旨在作為此目的的臨時網頁伺服器。Emrun 已預先設定為處理提供 gzip 壓縮檔案(尾碼為 .gz),並啟用對 Large-Allocation 標頭的支援,並允許編譯頁面的命令列自動化執行。

  • 捕獲來自調用編譯後的 asm.js 和 WebAssembly 程式碼的進入點內的所有例外狀況。編譯後的程式碼可以拋出三個不同的例外狀況類別

    1. C++ 例外狀況,由拋出的整數表示,且未被 C++ 程式捕獲。此整數指向應用程式堆積中包含指向拋出物件之指標的記憶體位置。

    2. 由 Emscripten 執行階段呼叫 abort() 函式引起的例外狀況。這些對應於編譯程式碼執行無法從中恢復的嚴重錯誤。例如,這可能會在呼叫無效的函式指標時發生。

    3. 由編譯後的 WebAssembly 程式碼造成的陷阱。這些對應於來自 WebAssembly VM 的嚴重錯誤。例如,當執行整數除以零的運算,或當將一個大的浮點數轉換為整數時,如果該浮點數超出該整數類型可表示的範圍,就可能發生這種情況。

  • 透過實作 window.onerror 腳本,在頁面上實作最終的「捕捉所有」錯誤處理程序。如果頁面上引發的例外狀況沒有其他來源處理,則會作為最後手段呼叫此程式碼。請參閱 MDN 上關於 window.onerror 的文件。

  • 不要讓 HTML 頁面「凍結」並將錯誤訊息埋在網頁主控台中,因為大多數使用者不知道如何在那裡找到它。盡力在主 HTML 頁面上向使用者提供有意義的錯誤報告,最好提供有關如何操作的提示。如果更新瀏覽器版本或 GPU 驅動程式,或釋放硬碟上的一些空間可能會有助於頁面執行,請讓使用者知道他們可以嘗試哪些方法。如果所發生的錯誤完全出乎意料,請考慮提供一個連結或電子郵件地址,以供回報問題。

  • 提供有意義且互動式的載入進度指示器,以向使用者顯示載入進度是否仍在進行以及接下來會發生什麼。盡量防止使用者陷入 「我不知道它是在載入中還是當掉了?」 的狀態。

為 Web 環境做好準備

在將網站發佈上線之前規劃測試矩陣時,以下項目可能是值得檢閱的好主意。

  • 當以頂層視窗執行時,與在 iframe 中執行時,網頁行為可能會略有不同。如果適用,請務必測試這兩種情況。

  • 測試 32 位元和 64 位元瀏覽器,特別是在 32 位元瀏覽器上模擬記憶體不足的情形。

  • 請注意 HTTP 跨來源資源共享規則,以及這些規則如何適用於您託管的網站架構。

  • 請注意 內容安全策略規則,並記下該網站計劃使用哪種類型的 CSP 策略。

  • 請注意瀏覽器強制執行的混合內容安全限制。

  • 請確保該網站在私密瀏覽(無痕)模式下也能良好運作。例如,這將防止網站將資料持久儲存到 IndexedDB。

  • 測試當頁面置於背景分頁時,是否能良好運作。使用 blurfocusvisibilitychange DOM 事件來回應頁面隱藏和顯示事件。這對於執行音訊播放的應用程式尤其相關。

  • 如果頁面使用 WebGL,請確保它能夠妥善處理 WebGL 上下文遺失事件。在測試時,使用 WebGL_lose_context 開發人員擴充功能以程式方式觸發上下文遺失事件。

  • 驗證頁面在具有不同 window.devicePixelRatio (DPI) 設定的顯示器上是否能按預期運作,尤其是在使用 WebGL 時。請參閱 Khronos.org:處理高 DPI。在 Windows 和 macOS 上,嘗試變更桌面顯示縮放設定,以測試瀏覽器回報的不同 window.devicePixelRatio 值。

  • 測試不同的頁面縮放層級不會破壞網站版面配置,尤其是在瀏覽器視窗已預先縮放的情況下瀏覽到頁面時。

  • 同樣地,驗證當調整瀏覽器視窗大小時,或當以非常小或大的尺寸,或以不成比例的長寬比訪問網站時,頁面版面配置不會損壞。

  • 特別是如果目標是行動裝置,請注意 <meta viewport> 標籤,瞭解如何開發在行動裝置上良好運作的網站版面配置。

  • 如果頁面使用 WebGL,請在目標平台上測試不同的 GPU。特別是,驗證在模擬缺少任何所需的 WebGL 擴充功能和壓縮紋理格式支援時,網站的行為是否正確。

  • 如果使用 requestAnimationFrame() API (即 emscripten_set_main_loop() 函數) 來驅動渲染,請注意,呼叫函數的速率不一定是 60 Hz,而可能會在執行時發生變化,例如,當在多顯示器設定中將瀏覽器視窗從一個顯示器移動到另一個顯示器時,如果顯示器具有不同的刷新率。75Hz、90Hz、100Hz、120Hz、144Hz 和 200Hz 等更新間隔正變得越來越普遍。

  • 模擬頁面可能需要的任何特殊 API 的缺失,例如 Gamepad、加速或觸控事件,並確保在這些情況下也能妥善處理適當的錯誤流程。

如果您有好的提示或建議可以分享,請透過將意見回饋張貼到 Emscripten 錯誤追蹤器emscripten-discuss 郵件論壇,以協助改進本指南。