欢迎你们收看聊一聊系列,这一套系列文章,能够帮助前端工程师们了解前端的方方面面(不只仅是代码):
https://segmentfault.com/blog...php
这一节,请跟随笔者聊一聊,网页的分段传输与渲染,用一些很是规手段优化咱们的网站响应速度。css
按照常理,咱们渲染一张网页,一定是网页所有拼装完毕,而后生成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>
图1.1.1nginx
上述例子,在本文后github中的normal文件夹中。git
如上所示,咱们能看到,直出的网页中,存在着后端数据串行,互相等待的尴尬局面。这也为咱们后续的优化埋下了伏笔。github
http1.1中引入了一个http首部,Transfer-Encoding:chunked。这个首部标识了实体采用chunked编码传输,chunked编码能够将实体分块儿进行传输,而且chunked编码的每一块内容都会自标识长度。这给了web开发者一个启示,若是须要多个数据,而多个数据均返回较慢的话。能够处理完一块就返回一块,让浏览器尽早的接收到html,能够先行渲染。web
既然知道了咱们能够将网页一起一起的传送,那么咱们就能够将上面的网页进行改造,拿好一起须要的数据,便渲染一起,无需等待,而模板方面,天然也要拆分为三段,供服务端拿一起的模板,就渲染一起出去,效果如图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>
图1.2.2.1
上述例子,在本文后github中的chunked文件夹中。
对比图图1.1.1与图1.2.2.1咱们能够发现,虽然最后总的处理时长不变,可是采用了分段输出的网页,能够尽早的将一段HTML渲染到客户端,这样用户可使用先到达的部分。另外一方面,尽早的页面反馈,也能够减小用户等待的焦躁情绪。综上,使用此种优化方法,能够提速网页的渲染速度。
咱们代码虽然如上所述,可是读者尝试的时候可能会发现,并无什么效果。和我截图并不同,仍是等到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均开启使用效果更佳哦~~~)。
图1.2.3.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();
图2.1.1
上述例子,在本文后github中的bigpipprepare文件夹中。
看完上述描述,读者们确定在想,若是能把最慢的部分放置于底部传过来就行了。因而有了一种加载思路,即是使用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>
图2.2.1
如上图,能够看到49ms的时候,就已经渲染出来了右侧两块儿,2S的时候,左侧也渲染出来了。
上述例子,在本文后github中的bigpipe文件夹中。
咱们刚刚作了一个实验,是将耗时最慢的块儿放在底部。然而,事实状况是,若是你也不知道哪块儿慢了呢?或者是,你的几块儿数据区块儿是并行的呢?出于刚刚的经验,咱们能够把页面上全部的块儿都架空,而后并行渲染,谁快谁就先渲染回填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>
图2.3.1
上述例子,在本文后github中的bigpipeparal文件夹中。
相信读着在此处会有疑问,为何慢的数据,不用ajax去请求呢?这样模板框架也能尽早的渲染出来。ajax毕竟是请求。相信不少读着也有这样的经历,后端处理若是遇到了瓶颈,那么有的时候咱们会选择同步页面渲染完以后,再发个请求去获取后端数据。可是笔者认为,这样作有必定弊端:
一、ajax毕竟是个请求,请求就要有链接,要有解析等过程。
二、服务端和客户端都会有闲的时候,发送ajax以前服务端闲,发送ajax出去以后,浏览器又闲着了。
因此,咱们使用bigpipe的方式仍是比多发送一个ajax有优点的。
笔者总结了一些使用分块儿传输比较合适的场景
1 前端须要尽早传输head中的一些css/js外联文件的状况下(能够先flush给客户端head前面的html内容,让浏览器尽早的去请求)
2 后端处理渲染的数据,上方较快,下方较慢的状况(能够先行渲染上方较快的部分)
对于更为复杂一点的bigpipe方式,若是上面的状况就适用于你的网站了的话,则最好采用简单的分块传输,不然以下状况,须要回填,则采用bigpipe方式渲染页面。毕竟,使用js回填仍是有性能损耗的。
1 后端有较慢的数据处理,阻塞住了页面的状况下,且最慢的部分不是在网页的最后。(能够把最慢的部分变为回填)
2 后端有多块儿数据要并行处理的状况下(你也不知道哪块儿先回来了,因此先渲染一个架子。对于并行的请求,先回来的先flush回填)
据笔者观察,新浪微博正是采用了bigpipe的方式进行渲染,如图3.3.1,咱们看到新浪微博的左侧导航栏与中间feed流区块儿都是架空的:
图3.3.1
在下方,有对左侧导航栏和中间feed流部分的回填,如图3.3.2
图3.3.2
因此,整个网页的渲染效果以下(如图3.3.3/图3.3.4/图3.3.5)
图3.3.3
图3.3.4
图3.3.5
笔者猜想,可能微博是并行渲染这几块儿的数据,因此采用了bigpipe的方式。
请读者们回想一下,本身的网站到底适不适合使用分块儿传输,可否使用上面的技术,使本身的网站更快一些呢?若是使用的话,是适合使用普通的chuned提速呢?仍是使用bigpipe进行提速呢?
若有说明不周的地方欢迎回复详询
本文中全部的例子,均在个人github上能够找到:
https://github.com/houyu01/ch...
接下来的一篇文章,我将会和读者们一块儿聊聊HTTPS那些事儿,不要走开,请关注我.....
https://segmentfault.com/a/11...
若是喜欢本文请点击下方的推荐哦,你的推荐会变为我继续更文的动力。
以上内容仅表明笔者我的观点,若有意见笔者愿意学习参考各读者的建议。