document.write 的痛

1. 代碼執行是嚴格遵循順序的

不廢話,先看下面代碼和執行結果

<script src="1.js"></script><script src="2.js"></script><script src="3.js"></script><script>document.write(<!--);</script><script src="4.js"></script><script src="5.js"></script><script src="6.js"></script>

瀏覽器對資源載入是有優化的,當頁面上存在很多個 js 文件時,它們會同時載入(注意載入並不意味著執行)。上面這個例子中雖然 6 個 js 文件都同時載入,但是由於中間被 script 插入了一行 html 注釋,後面的 4、5、6 即便載入了也沒有被執行。

那麼如果插入的不是注釋而是別的 js 文件呢?再看另一段代碼

<script src="1.js"></script><script src="2.js"></script><script src="3.js"></script><script>document.write(<script src="4.js"></script>);</script><script src="5.js"></script><script src="6.js"></script>

瀏覽器依然優化了直接寫在 html 文件中的 script 引用,它們是同時載入的。但是 4 是在 script 中插入到文檔的,而代碼執行是嚴格遵循順序的,要執行這個 script 就必須等到 1、2、3 載入並執行完畢後才能夠開始執行。所以 4 會在 1、2、3 執行完畢後才開始載入。4 插入到文檔的位置是在 5 和 6 之前,所以 5 和 6 雖然很早就載入完畢,但也必須等待 4 執行完畢後才能夠執行。

直接觀察到的現象就是 document.write 阻斷了後面代碼的執行。但阻斷代碼執行的根本原因應該是代碼執行順序。這是可以想辦法繞過去的。比如把 4 的載入放在所有 script 的最前面,或者更暴力的辦法是所有資源都通過 document.write 出來:

<script>document.write([ <script src="1.js"></script>, <script src="2.js"></script>, <script src="3.js"></script>, <script src="4.js"></script>, <script src="5.js"></script>, <script src="6.js"></script>].join(
));</script>

這代碼雖然看起來很醜,但實際執行速度和完全不使用 document.write 是幾乎沒差別的。

2. 一波來自 Chrome 的優化

從 Chrome 55 開始,使用 document.write 來載入跨域腳本時瀏覽器會拋出警告。

在 2G 網路環境下,document.write 的這種使用姿勢會被 Chrome 智能(zhàng)地「優化」掉。

見:chromestatus.com/featur

雖然前面的種種實驗還了 document.write 的清白,但是它還是沒有逃過潮流的趨勢。

既然如此,我們只能想辦法找到對應的替代方案。

3. document.write 存在的場景以及替代方案

從現狀看來,會用到 document.write 的唯一場景就是帶條件的資源文件同步載入。

比如為了兼容不支持某些特性的瀏覽器需要載入對應的 Polyfill,但是又考慮到自身本就支持的瀏覽器,所以要做兼容性檢測,只針對不支持的瀏覽器載入 Polyfill。

<script>if (!window.Promise) { document.write(<script src="polyfill/promise.js"></script>);}if (!window.fetch) { document.write(<script src="polyfill/fetch.js"></script>);}</script>

還有一些與具體平台相關的代碼也會考慮同步按需引入,比如:

<script>if(/MicroMessenger/i.test(navigator.userAgent)) { document.write(<script src="jweixin-1.2.0.js"></script>);}if(/AlipayClient/i.test(navigator.userAgent)) { document.write(<script src="alipayjsapi.min.js"></script>);}</script>

對於以上兩種場景都有一些對應的替代方案:

將檢測的邏輯搬到伺服器端

Polyfill 的載入可以通過 UA 檢測到瀏覽器的版本,根據兼容性列表可以查出客戶端使用的瀏覽器是否支持某些特性,然後再決定要不要載入這些 Polyfill。如果需要載入,伺服器端代碼直接往 html 里塞入對應的 script 標籤。

平台相關的代碼載入同樣可以在伺服器端直接通過 UA 中的關鍵字來決定要不要在 html 文件放對應的 script 標籤。

用多入口解決多平台問題

為每個平台打包專用的入口。比如一個頁面如果要同時在微信和支付寶中運行,在前端項目構建時就可以打包 3 個入口文件,一個給微信專用,一個給支付寶專用,最後再打包一個通用的。在這個通過的版本中做一些環境檢測並跳轉到正確版本的邏輯,這樣也可以解決問題。

放棄 CDN

如果不使用 CDN 其實不會有那麼多問題,Chrome 對 document.write 限制僅針對跨域,如果不用 CDN 就很容易做到同域請求資源,繞過 Chrome 的限制。當然,我們的目的是找替代方案,而不是找繞行方法,你可以忽略這段。

記得以前見過一個方案,是伺服器端提供一個 polyfill.js,然後這個 js 文件是動態的,裡面檢測了瀏覽器版本等信息,然後輸出對應的 Polyfill 代碼。這個想法很不錯,可遺憾的是國內的 CDN 並不支持通過 http 的 Vary 頭來計算緩存的 key,所以這個方案只能被歸為「放棄 CDN」的方案之一。

4. 總結

  • 代碼被阻斷執行是因為要確保順序,並不是 document.write 的鍋。
  • 從趨勢上看起來 document.write 未來會被拋棄,我們應該積極尋找替代方案。

我就是塊磚,如果大家有什麼更神奇的方案歡迎分享。


推薦閱讀:

MySQL性能優化的最佳21條經驗
Weex實戰分享|Weex在盛大遊戲中的應用實踐

TAG:前端開發 | 性能優化 |