[聊一聊系列]聊一聊網頁的分段傳輸與渲染那些事兒

歡迎你們收看聊一聊系列,這一套系列文章,能夠幫助前端工程師們瞭解前端的方方面面(不只僅是代碼):
https://segmentfault.com/blog...php

這一節,請跟隨筆者聊一聊,網頁的分段傳輸與渲染,用一些很是規手段優化咱們的網站響應速度。css

1 CHUNKED編碼

1.1 傳統的渲染方法

1.1.1 傳統的渲染方法怎麼作?

按照常理,咱們渲染一張網頁,一定是網頁所有拼裝完畢,而後生成HTML字符串,傳送至客戶端。這也意味着,若是一張網頁處理的有快有慢的話,必須串行等到全部的邏輯都處理完畢。後端才能進行返回。(這也是咱們目前網頁的通常邏輯)。以下面的例子,三個很慢的讀數據操做,均執行完畢後,才傳送渲染頁面。渲染效果如圖1.1.1,15s以後才傳送並渲染出頁面:
normal.phphtml

<?php
function getOneData() {
    usleep(5000000);
    return '我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出>的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出>的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出>的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出>的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據'; 
}

$var1 = getOneData();

function getTwoData() {
    usleep(5000000);
    return '是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據';
}
$var2 = getTwoData();

function getThreeData() {
    usleep(5000000);
    return '我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出>的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出>的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出>的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出>的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據'; 
}
$var3 = getThreeData();

// 渲染模板並輸出
include('./normal.html.php');

normal.html.php前端

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <div>1. <?php echo $var1;?></div>
        <div>2. <?php echo $var2;?></div>
        <div>3. <?php echo $var3;?></div>
    </body>
</html>

clipboard.png
圖1.1.1nginx

上述例子,在本文後github中的normal文件夾中。git

1.1.2 傳統的渲染方法有哪些弊端?

如上所示,咱們能看到,直出的網頁中,存在着後端數據串行,互相等待的尷尬局面。這也爲咱們後續的優化埋下了伏筆。github

1.2 分段傳輸

1.2.1 何爲分段傳輸?

http1.1中引入了一個http首部,Transfer-Encoding:chunked。這個首部標識了實體採用chunked編碼傳輸,chunked編碼能夠將實體分塊兒進行傳輸,而且chunked編碼的每一塊內容都會自標識長度。這給了web開發者一個啓示,若是須要多個數據,而多個數據均返回較慢的話。能夠處理完一塊就返回一塊,讓瀏覽器儘早的接收到html,能夠先行渲染。web

1.2.2 如何分段傳輸?

既然知道了咱們能夠將網頁一起一起的傳送,那麼咱們就能夠將上面的網頁進行改造,拿好一起須要的數據,便渲染一起,無需等待,而模板方面,天然也要拆分爲三段,供服務端拿一起的模板,就渲染一起出去,效果如圖1.2.2.1。
normal.phpajax

<?php
function getOneData() {
    usleep(5000000);
    return '我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出>的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出>的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出>的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出>的第一個數據我是取出的第一個數據我是取出的第一個數據我是取出的第一個數據';
}
// 取出第一起的數據
$var1 = getOneData();
// 渲染第一起
include('./normal1.html.php');
//刷新到緩衝區,渲染第一份兒模板,傳送到客戶端
ob_flush();
flush();

function getTwoData() {
    usleep(5000000);
    return '我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出>的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出>的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出>的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出>的第二個數據我是取出的第二個數據我是取出的第二個數據我是取出的第二個數據';
}
// 取出第二塊兒的數據
$var2 = getTwoData();
// 渲染第二塊兒
include('./normal2.html.php');
//刷新到緩衝區,渲染第二份兒模板,傳送到客戶端
ob_flush();
flush();

function getThreeData() {
    usleep(5000000);
    return '我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出>的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出>的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出>的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出>的第三個數據我是取出的第三個數據我是取出的第三個數據我是取出的第三個數據';
}
// 獲取第三塊兒的數據
$var3 = getThreeData();
// 渲染第三塊兒
include('./normal3.html.php');
// 將第三份兒的模板,傳送到客戶端
ob_flush();
flush();

normal1.html.phpsegmentfault

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <div>1. <?php echo $var1;?></div>

normal2.html.php

<div>2. <?php echo $var2;?></div>

normal3.html.php

<div>3. <?php echo $var3;?></div>
    </body>
</html>

clipboard.png
圖1.2.2.1
上述例子,在本文後github中的chunked文件夾中。

對比圖圖1.1.1與圖1.2.2.1咱們能夠發現,雖然最後總的處理時長不變,可是採用了分段輸出的網頁,能夠儘早的將一段HTML渲染到客戶端,這樣用戶可使用先到達的部分。另外一方面,儘早的頁面反饋,也能夠減小用戶等待的焦躁情緒。綜上,使用此種優化方法,能夠提速網頁的渲染速度。

1.2.3 分段傳輸小TIPs

咱們代碼雖然如上所述,可是讀者嘗試的時候可能會發現,並無什麼效果。和我截圖並不同,仍是等到15s後一塊兒渲染出來了。這裏要提醒你們一下,多是因爲nginx配置的緣由。若是使用的是nginx作server的話,要使用以下配置才能看到效果。

http {
    ....
    fastcgi_buffer_size 1k; 
    fastcgi_buffers 16 1k; 
    gzip  off;
    ....
}

其實讀者們能夠這麼理解上面的配置,nginx會在攢夠一起緩衝區的量後,能夠將一起數據發出去。上面咱們配置了fastcgi_buffers 16 1k; 就是16塊兒,大小爲1K的緩存。

咱們的數據量過小了,連默認的一起緩衝區都填不滿,無法看到分塊兒發送的效果,因此這裏咱們將緩衝區給調小爲1K,這樣就能1K爲單位分塊兒,1K一發,體現出實驗效果了。筆者這裏建議作實驗的時候,最好把gzip給關了,由於,我們作實驗的時候數據量不大,實際使用中建議chunked與gzip均開啓(如圖1.2.3.1,若是量比較大的話,gzip與chunked均開啓使用效果更佳哦~~~)。
clipboard.png
圖1.2.3.1

1.2.4 分段傳輸適用場景

當頁面的某些後端處理比較耗時的時候,能夠試試採用分段傳輸,能夠渲染一部分,就發送一部分到客戶端,雖然總時長不變,可是瀏覽器在所有傳輸完以前不會處於乾等狀態。能夠儘早的渲染並給予用戶反饋。

2 BIGPIPE

2.1 分段傳輸的侷限

剛剛筆者和讀者們一塊兒作了分段傳輸的實驗,思路是基於讀者們想展現的網頁也是上快下慢的。但是讀者們有沒有想過,若是整個網頁中,最快的是下方,而最慢的是上方呢?這樣咱們就沒法利用分段傳輸的優點了嗎?如圖2.1.1,整個頁面依舊是被最慢的第一部分數據渲染給hold住了。然後兩塊兒渲染較快,徹底能夠先傳輸過來。

<?php
// 獲取第一起數據最慢
function getOneData() {
    usleep(2000000);
    $str = ''; 
    for ($i = 0; $i < 500; $i++) {
        $str .= '我是取出的第一個數據';
    }   
    return $str;
}

$var1 = getOneData();
// 渲染第一起
include('./normal1.html.php');
ob_flush();
flush();

// 獲取第二塊兒數據較快
function getTwoData() {
    $str = ''; 
    for ($i = 0; $i < 500; $i++) {
        $str .= '我是取出的第二個數據';
    }   
    return $str;
}
$var2 = getTwoData();
// 渲染第二塊兒
include('./normal2.html.php');
ob_flush();
flush();

// 獲取地三塊兒數據也較快
function getThreeData() {
    $str = ''; 
    for ($i = 0; $i < 500; $i++) {
        $str .= '我是取出的第三個數據';
    }   
    return $str;
}
$var3 = getThreeData();
// 渲染第三塊兒
include('./normal3.html.php');
ob_flush();
flush();

clipboard.png
圖2.1.1

上述例子,在本文後github中的bigpipprepare文件夾中。

2.2 解決分段傳輸順序的問題

看完上述描述,讀者們確定在想,若是能把最慢的部分放置於底部傳過來就行了。因而有了一種加載思路,即是使用js回填的方式,先將左邊最慢的部分架空,而後在底部寫上js回填。這樣不就能夠先渲染相對較快的右側兩塊兒了麼。如圖2.2.1
後端能夠先渲染快的模板,而後再渲染最慢的模板。

<?php
// 渲染第一起的架子,還未獲取內容
include('./normal1.html.php');
ob_flush();
flush();

// 獲取第二塊兒數據較快
function getTwoData() {
    $str = ''; 
    for ($i = 0; $i < 50; $i++) {
        $str .= '我是取出的第二個數據';
    }   
    return $str;
}
$var2 = getTwoData();

// 渲染第二塊兒
include('./normal2.html.php');
ob_flush();
flush();

// 獲取地三塊兒數據也較快
function getThreeData() {
    $str = ''; 
    for ($i = 0; $i < 70; $i++) {
        $str .= '我是取出的第三個數據';
    }   
    return $str;
}
$var3 = getThreeData();
// 渲染第三塊兒
include('./normal3.html.php');
ob_flush();
flush();

// 獲取第一起數據最慢
function getOneData() {
    usleep(2000000);
    $str = ''; 
    for ($i = 0; $i < 50; $i++) {
        $str .= '我是取出的第一個數據';
    }   
    return $str;
}
$var1 = getOneData();
// 渲染回填第一起
include('./normal4.html.php');
ob_flush();
flush();

normal1.html.php

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8" />
        <style>
            html, body {
                margin: 0;
            }   
            .part1 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #0f0;
                outline: 1px solid #000;
            }   
            .part2 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #f00;
                outline: 1px solid #000;
            }   
            .part3 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #00f;
                outline: 1px solid #000;
            }   
        </style>
    </head>
    <body>
        <div class="part1">
        </div>

normal2.html.php

<div class="part2">2. <?php echo $var2;?></div>

normal3.html.php

<div class="part3">3. <?php echo $var3;?></div>

normal4.html.php

<script>
            // 把最慢且頂在前面的部分用js回填回去
            document.querySelector('.part1').innerHTML = "<?php echo $var1?>";
        </script>
    </body>
</html>

clipboard.png
圖2.2.1
如上圖,能夠看到49ms的時候,就已經渲染出來了右側兩塊兒,2S的時候,左側也渲染出來了。

上述例子,在本文後github中的bigpipe文件夾中。

2.3 回填思路的擴展與並行化

咱們剛剛作了一個實驗,是將耗時最慢的塊兒放在底部。然而,事實狀況是,若是你也不知道哪塊兒慢了呢?或者是,你的幾塊兒數據區塊兒是並行的呢?出於剛剛的經驗,咱們能夠把頁面上全部的塊兒都架空,而後並行渲染,誰快誰就先渲染回填js。這樣就能夠達到並行且先到先渲染的目的了。我這裏作了個php並行取並回填的實驗,如圖2.3.1,能夠看到,中間紅色的雖然被阻塞,可是框架先行渲染出來了全部的內容均是空的。綠色最快,先行回填渲染了出來,藍色稍慢,也跟着渲染了出來,最後紅色完畢,回填渲染結束了。
並行渲染的PHP(normal.php)

<?php
function asyncRequest($host, $url, $port=8082, $conn_timeout=30, $rw_timeout=86400) {
    $errno = ''; 
    $errstr = ''; 
    $fp = fsockopen($host, $port, $errno, $errstr, $conn_timeout);
    if (!$fp) {
       echo "Server error:$errstr($errno)";
       return false;
    }   
    stream_set_timeout($fp, $rw_timeout);
    stream_set_blocking($fp, false);

    $rq = "GET $url HTTP/1.0\r\n";
    $rq .= "Host: $host\r\n";
    $rq .= "Connect: close\r\n\r\n";
    fwrite($fp, $rq);
    return $fp;
}

function asyncFetch(&$fp) {
   if ($fp === false) return false;

   if (feof($fp)) {
      fclose($fp);
      $fp = false;
      return false;
   }   
   return fread($fp, 10000);
}

$fp1 = asyncRequest('localhost', '/bigpipeparal/data1.php');
$fp2 = asyncRequest('localhost', '/bigpipeparal/data2.php');
$fp3 = asyncRequest('localhost', '/bigpipeparal/data3.php');

include('normal_frame.html.php');
ob_flush();
flush();
while (true) {
    sleep(1);
    $r1 = asyncFetch($fp1);
    $r2 = asyncFetch($fp2);
    $r3 = asyncFetch($fp3);
    //誰快誰先渲染並flush刷出
    if ($r1 != false) {
        preg_match('/\|(.+)\|/i', $r1, $res);
        $var1 = $res[1];
        include('normal1.html.php');
    }

    if ($r2 != false) {
        preg_match('/\|(.+)\|/i', $r2, $res);
        $var2 = $res[1];
        include('normal2.html.php');
    }

    if ($r3 != false) {
        preg_match('/\|(.+)\|/i', $r3, $res);
        $var3 = $res[1];
        include('normal3.html.php');
    }

    if ($r1 == false && $r2 == false && $r3 == false) {
        break;
    }
  
    ob_flush();
    flush();
}

主框架的模板,架空,等待回填。normal_frame.html.php

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="utf-8" />
        <style>
            html, body {
                margin: 0;
            }   
            .part1 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #0f0;
                outline: 1px solid #000;
            }   
            .part2 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #f00;
                outline: 1px solid #000;
            }   
            .part3 {
                vertical-align: top;
                display: inline-block;
                width: 200px;
                background: #00f;
                outline: 1px solid #000;
            }   
        </style>
    </head>
    <body>
        <!--三塊兒所有架空,等待回填--> 
        <div class="part1"></div>
        <div class="part2"></div>
        <div class="part3"></div>
    </body>
</html>

具體回填模板,normal1.html.php/normal2.html.php/normal3.html.php

<script>
    document.querySelector('.part1').innerHTML = "第一起回填!其值以下:<?php echo $var1?>";
</script>

clipboard.png
圖2.3.1

上述例子,在本文後github中的bigpipeparal文件夾中。

2.4 爲何不用ajax?

相信讀着在此處會有疑問,爲何慢的數據,不用ajax去請求呢?這樣模板框架也能儘早的渲染出來。ajax畢竟是請求。相信不少讀着也有這樣的經歷,後端處理若是遇到了瓶頸,那麼有的時候咱們會選擇同步頁面渲染完以後,再發個請求去獲取後端數據。可是筆者認爲,這樣作有必定弊端:
一、ajax畢竟是個請求,請求就要有鏈接,要有解析等過程。
二、服務端和客戶端都會有閒的時候,發送ajax以前服務端閒,發送ajax出去以後,瀏覽器又閒着了。
因此,咱們使用bigpipe的方式仍是比多發送一個ajax有優點的。

3 分段傳輸與bigpipe適用場景

3.1 分段傳輸的適用場景

筆者總結了一些使用分塊兒傳輸比較合適的場景
1 前端須要儘早傳輸head中的一些css/js外聯文件的狀況下(能夠先flush給客戶端head前面的html內容,讓瀏覽器儘早的去請求)
2 後端處理渲染的數據,上方較快,下方較慢的狀況(能夠先行渲染上方較快的部分)

3.2 使用bigpipe的場景

對於更爲複雜一點的bigpipe方式,若是上面的狀況就適用於你的網站了的話,則最好採用簡單的分塊傳輸,不然以下狀況,須要回填,則採用bigpipe方式渲染頁面。畢竟,使用js回填仍是有性能損耗的。
1 後端有較慢的數據處理,阻塞住了頁面的狀況下,且最慢的部分不是在網頁的最後。(能夠把最慢的部分變爲回填)
2 後端有多塊兒數據要並行處理的狀況下(你也不知道哪塊兒先回來了,因此先渲染一個架子。對於並行的請求,先回來的先flush回填)

3.3 國內的應用

據筆者觀察,新浪微博正是採用了bigpipe的方式進行渲染,如圖3.3.1,咱們看到新浪微博的左側導航欄與中間feed流區塊兒都是架空的:
clipboard.png
圖3.3.1
在下方,有對左側導航欄和中間feed流部分的回填,如圖3.3.2
clipboard.png
圖3.3.2

因此,整個網頁的渲染效果以下(如圖3.3.3/圖3.3.4/圖3.3.5)
clipboard.png
圖3.3.3
clipboard.png
圖3.3.4
clipboard.png
圖3.3.5
筆者猜想,可能微博是並行渲染這幾塊兒的數據,因此採用了bigpipe的方式。

4 課後做業

請讀者們回想一下,本身的網站到底適不適合使用分塊兒傳輸,可否使用上面的技術,使本身的網站更快一些呢?若是使用的話,是適合使用普通的chuned提速呢?仍是使用bigpipe進行提速呢?

若有說明不周的地方歡迎回復詳詢

本文中全部的例子,均在個人github上能夠找到:
https://github.com/houyu01/ch...

接下來的一篇文章,我將會和讀者們一塊兒聊聊HTTPS那些事兒,不要走開,請關注我.....

https://segmentfault.com/a/11...

若是喜歡本文請點擊下方的推薦哦,你的推薦會變爲我繼續更文的動力。

以上內容僅表明筆者我的觀點,若有意見筆者願意學習參考各讀者的建議。

相關文章
相關標籤/搜索