轉載:[譯] 內容加速黑科技趣談

原文做者:@Jake Archibald
原文地址:https://jakearchibald.com/201...
中文翻譯:文藺
譯文地址:http://www.wemlion.com/2017/f...
蒙原做者惠允翻譯發佈,轉載請保留此聲明。
著做權屬於原做者,本譯文僅用於學習、研究和交流目的,請勿用於商業目的。html

有一說一(Show them what you got)

頁面加載的時候,瀏覽器會接收網絡數據流,並將其輸出(pipe)給 HTML 解析器,HTML 解析器再將數據輸出到文檔。這意味着,頁面是邊加載邊渲染的。對於一個 100k 的頁面來講,瀏覽器極可能在接收到 20k 數據的時候就開始渲染出一些可用內容了。json

這個偉大又古老的特性,經常被開發者們有意無心地忽略了。多數提升加載性能的建議都歸結於一點,即「展現你所拿到的東西」 —— 別怕,千萬不要傻傻等待一切加載完成以後再去展現內容。瀏覽器

GitHub 固然是關注性能的,因此他們使用服務端渲染。但在同一個 tab 下瀏覽頁面時,他們用 JavaScript 從新實現了導航(navigation)功能,相似下面這樣:服務器

// …一堆從新實現瀏覽器導航功能代碼…
const response = await fetch('page-data.inc');
const html = await response.text();
document.querySelector('.content').innerHTML = html;
// …加載更多從新實現導航功能的代碼…

這違反了規則,由於在 page-data.inc 下載完成以前什麼事情都沒幹。而服務端渲染版徹底不會這樣囤積內容,其內容是流式的,這樣就要快得多了。就 Github 的客戶端渲染來講,不少 JavaScript 代碼徹底減慢了渲染過程。網絡

這裏我僅僅只是拿 Github 舉例子 —— 這種反模式在單頁應用中比比皆是。app

在頁面以內切換內容可能確實有些好處,特別是存在大量腳本的狀況下,無需從新執行所有腳本便可更新內容。但咱們可否在不放棄流的狀況下完成這樣的工做呢?我曾常常說 JavaScript 沒有辦法對流進行解析,但其實仍是有的……框架

<iframe> 和 document.write 大法

iframe 早已躋身圈內最臭黑科技之列。但下面這個辦法就使用了 iframe 和 document.write(),這樣咱們就能將內容以流的形式添加到頁面中了。示例以下:異步

// 建立 iframe:
const iframe = document.createElement('iframe');
// 添加到 document 中 (記得隱藏起來):
iframe.style.display = 'none';
document.body.appendChild(iframe);
// 等待 iframe 加載:
iframe.onload = () => {
  // 忽略其餘 onload 操做:
  iframe.onload = null;
  // 添加一個虛擬標籤:
  iframe.contentDocument.write('<streaming-element>');
  // 引用該元素:
  const streamingElement = iframe.contentDocument.querySelector('streaming-element');
  // 將該元素從 iframe 中取出,並添加到文檔中:
  document.body.appendChild(streamingElement);
  // 寫入一些內容 —— 這裏應該是異步的:
  iframe.contentDocument.write('<p>Hello!</p>');
  // 繼續寫入內容,直到完成:
  iframe.contentDocument.write('</streaming-element>');
  iframe.contentDocument.close();
};
//  iframe 初始化
iframe.src = '';

雖然 Hello! 是寫到 iframe 中的,但它卻出如今了父級的 document 中!這是由於解析器維護了一個敞開元素棧(stack of open elements),新建立的元素會被壓入棧中。就算咱們把 <streaming-element/> 元素移出到 iframe 外面也不影響,就是這麼任性。性能

此外,這種技術處理起 HTML 來,要比 innerHTML 更接近標準的頁面加載解析器。尤爲是腳本依然會被下載,並在父級文檔的上下文中執行 —— 只是在 Firefox 中徹底不會執行,但我認爲這是個 bug更新: 其實腳本根本不該該執行(感謝 Simon Pieters 指出這一點),但 Edge、Safari、Chrome 都這麼幹。學習

接下來咱們只須要從服務端獲取 HTML 數據流,每當一個部分的數據到達的時候,就調用 iframe.contentDocument.write()。流式傳輸和 fetch() 搭配起來會更好,但爲了支持 Safari,咱們仍是使用 XHR 來 hack 一下吧。

我已經寫好了一個 demo,能夠拿來和 Github 進行對比。下面是在 3G 網絡下的測試結果:
這裏寫圖片描述
使用 iframe 進行流式渲染,頁面加載速度提升了 1.5 s。頭像也提早半秒鐘加載完成 —— 流式渲染意味着瀏覽器能夠更早發現它們,並與內容一塊兒並行下載。

上面的方法對 Github 來講仍是有效的,由於它的服務器返回的是 HTML。若是你使用的是框架,由框架本身管理 DOM 的展現,那可能就麻煩一些了。這種狀況下能夠看看下面這個次優選項:

換行符分隔的 JSON(Newline-delimited JSON)

許多網站使用 JSON 驅動動態內容。何其不幸,JSON 並非一種對流友好的格式。儘管也有流式 JSON 解析器,可用起來卻並不那麼簡單。

因此與其傳輸下面這樣一大塊 JSON 數據:

{
  "Comments": [
    {"author": "Alex", "body": "…"},
    {"author": "Jake", "body": "…"}
  ]
}

還不如像下面這樣一行輸出一個 JSON 對象:

{"author": "Alex", "body": "…"}
{"author": "Jake", "body": "…"}

這種被稱爲 「換行符分隔的 JSON」 是有標準的:ndjson。給上面的內容寫一個解析器就要簡單多了。到了 2017 年,咱們也許可使用一系列組合變換流(composable transform streams)來描述(譯者注:本文寫做於 2016 年 12 月):

// 在 2017 年的某個時候可能會是這樣:
const response = await fetch('comments.ndjson');
const comments = response.body
  // 從字節到文本:
  .pipeThrough(new TextDecoder())
  // 一直緩衝,直到遇到換行符:
  .pipeThrough(splitStream('\n'))
  // 將內容塊解析爲JSON:
  .pipeThrough(parseJSON());
for await (const comment of comments) {
  // 處理每條評論,並將其添加到頁面:
  // (無論你使用的是什麼模板或虛擬 DOM)
  addCommentToPage(comment);
}

在上面的代碼中,splitStream 和 parseJSON 是可複用變換流(reusable transform streams)。與此同時,爲了實現最大程度的兼容,咱們可使用 XHR 進行 hack。

我再次新建了一個對比的 demo,下面是 3G 網絡下的結果:
這裏寫圖片描述
與常規 JSON 相比,ND-JSON 提早 1.5s 將內容渲染到頁面上,儘管速度不如 iframe 方法那麼快。在建立元素以前,必須等待完整的 JSON 對象出現。若是你的 JSON 文件體量巨大,可能會陷入對流的企盼之中。

單頁應用?彆着急

如前所述,Github 使用了大量的代碼,然而卻帶來這樣的性能問題。在客戶端從新實現導航功能是困難的,若是你須要改變頁面中的大塊內容,這麼作有可能並不值得。

能夠拿咱們的嘗試與簡單瀏覽器導航進行對比:這裏寫圖片描述打開一個簡單的沒有使用 JavaScript 瀏覽器導航的服務端渲染頁面的速度差很少是同樣的。但除去評論列表,測試頁面實在太過簡單。若是在不一樣頁面之間存在有大量重複的複雜內容(主要是指可怕的廣告腳本),結果可能因實際狀況而有差別,但必定要記得進行測試!極可能你編寫了一大堆代碼,然而只能帶來少的可憐的提高,甚至還可能減慢速度。

相關文章
相關標籤/搜索