高性能頁面加載技術--BigPipe設計原理及Java簡單實現

1.技術背景

  動態web網站的歷史能夠追溯到萬維網初期,相比於靜態網站,動態網站提供了強大的可交互功能.通過幾十年的發展,動態網站在互動性和頁面顯示效果上有了很大的提高,可是對於網站動態網站的總體頁面加載架構沒有作太大的改變.對於用戶而言,頁面的加載速度極大的影響着用戶體驗感.與靜態網站不一樣,除了頁面的傳輸加載時間外,動態網站還需考慮服務端數據的處理時間.像facebook這樣大型的用戶社交網站,必須考慮用戶訪問速度問題,javascript

  傳統web模式採用了順序處理的流程來處理用戶請求.即用戶向客戶端發送一個請求後,服務器處理請求,加載數據並渲染頁面;最後將頁面返回給客戶端.整個過程是串行執行的,具體流程以下:css

  1. 瀏覽器發送一個HTTP請求到Web服務器。
  2. Web服務器解析請求,而後讀取數據存儲層,制定一個HTML文件,並用一個HTTP響應把它發送到客戶端。
  3. HTTP響應經過互聯網傳送到瀏覽器。
  4. 瀏覽器解析Web服務器的響應,使用HTML文件構建了一個的DOM樹,而且下載引用的CSS和JavaScript文件。
  5. CSS資源下載後,瀏覽器解析它們,並將它們應用到DOM樹。
  6. JavaScript資源下載後,瀏覽器解析並執行它們。html

  整個流程按必須順序執行,不能重疊,這也是爲何傳統模式隨着網絡速度的提高訪問速度沒有很大提高的緣由.解決順序執行的速度問題,通常能想到的就是多線程併發執行.Facebook 的前端性能研究小組採用瀏覽器和服務端併發執行的思路,通過了六個月的努力,開發出了BigPipe頁面異步加載技術,成功的將我的空間主頁面加載耗時由原來的5 秒減小爲如今的2.5 秒。這就是咱們本文要介紹的高性能頁面加載技術---BigPipe.前端

2.BigPipe設計原理

  BigPipe的主要思想是實現瀏覽器和服務器的併發執行,實現頁面的異步加載從而提升頁面加載速度.爲了達到這個目的,BigPipe首先根據頁面的功能或位置將一個頁面分紅若干模塊(模塊稱做pagelet),並對這幾個模塊進行標識.舉個例子,在博客園我的首頁包括幾大板塊,如頭部信息,左邊信息,博文列表,footer等.咱們能夠將首頁按這些功能分塊,並用惟一id或名稱標識pagelet.客戶端向服務端發送請求後(發出一次訪問請求,如請求訪問我的博客首頁),服務端採用併發形式獲取各個pagelet的數據並渲染pagelet的頁面效果.一旦某個pagelet頁面渲染完成則馬上採用json形式將該pagelet頁面顯示結果返回給客戶端.客戶端瀏覽器會根據pagelet的id或標識符,在頁面的制定區域對pagelet進行轉載渲染.客戶端的模塊加載採用js技術.具體流程以下:java

   1. 請求解析:Web服務器解析和完整性檢查的HTTP請求。
   2. 數據獲取:Web服務器從存儲層獲取數據。
   3. 標記生成:Web服務器生成的響應的HTML標記。
   4. 網絡傳輸:響應從Web服務器傳送到瀏覽器。
   5. CSS的下載:瀏覽器下載網頁的CSS的要求。
   6. DOM樹結構和CSS樣式:瀏覽器構造的DOM文檔樹,而後應用它的CSS規則。
   7. JavaScript中下載:瀏覽器下載網頁中JavaScript引用的資源。
   8. JavaScript執行:瀏覽器的網頁執行JavaScript代碼python

  前三個階段由Web服務器執行,最後四個階段(5,6,7,8)是由瀏覽器執行。因此在服務器能夠採用多線程併發方式對每一個pagelet進行數據獲取和標記生成頁面,生成好的pagelet頁面發送給前端.同時在瀏覽器端,對css,js的下載能夠採用並行化處理.這就達到了瀏覽器和服務器的併發執行的效果,這樣使得多個流程能夠疊加執行,加少了總體頁面的加載時間.瀏覽器端的並行化交給瀏覽器本身處理不須要咱們作額外工做.在BigPipe中主要是處理服務端的並行性.git

3. BigPipe的實現原理

  在BigPipe,一個用戶請求的生命週期是這樣的:在瀏覽器發送一個HTTP請求到Web服務器。在收到的HTTP請求,並在上面進行一些全面的檢查, 網站服務器當即發回一個未關閉的HTML文件,其中包括一個HTML 標籤和標籤的開始標籤。標籤包括BigPipe的JavaScript庫來解析Pagelet之後收到的答覆。在標籤,有一個模板,它指定了頁面的邏輯結構和Pagelets佔位符。例如:github

 1 <html>
 2 <head>
 3     <title>struts2-bigpipe-plugin</title>
 4     <script type="application/javascript">
 5            function replace(id, content) {
 6                  var pagelet = document.getElementById(id);
 7                  pagelet.innerHTML = content;
 8            }
 9     </script>
10 </head>
11 <body>
12 <table width="100%">
13     <tr border="1">
14         <td width="50%" bgcolor="#f0f8ff"><div id="one">${one}</div></td>
15         <td width="50%" bgcolor="#faebd7"><div id="two">${two}</div></td>
16     </tr>
17     <tr>
18         <td width="50%" bgcolor="#7fffd4"><div id="three">${three}</div></td>
19         <td width="50%" bgcolor="#8a2be2"><div id="four">${four}</div></td>
20     </tr>
21 </table>
View Code

  注意:這個html沒有以</body> </html>結束,這是一個未關閉的頁面模板(這裏定義爲index.ftl,是freemarker模板).若是是封閉的頁面,那麼瀏覽器就不會等待也不接收服務器以後返回的數據.因此這裏必須設置爲未關閉的頁面.web

  服務端返回給客戶端頁面模板(index.ftl)後,並行的處理各個pagelet的數據並和將數據填補pagelet對應的頁面顯示模板(以下面代碼的one.ftl)中獲得一個渲染完成的頁面.若是某個pagelet的頁面渲染完成,那麼就將pagelet的id或標示符,html內容及相關數據組成json數據馬上發送給客戶端.客戶端根據pagelet的id和html內容,以及以前傳回來的模板(index.ftl),利用模板(index.ftl)中的JavaScript解析函數將pagelet的數據加載到頁面對應的位置.下面是對應上面模板中id爲one的pagelet的頁面模板(one.ftl):spring

<h1>Part One</h1>
<h2>你好:${user.name},如今時間時 ${time}  </h2>

  服務端將數據填充到one.ftl頁面後,將構建json數據,並當即將json數據發送給客戶端(瀏覽器).下面是服務器發回給客戶端的pagelet one對應的數據.爲了使解析的js書寫簡單(index.ftl的js解析方法replace()),這裏沒有直接轉換爲json,而是用html數據返回給前端:

<script type="application/javascript\">
       replace("one",              
              "<h1>Part One</h1>
                <h2>你好:John,如今時間時 23:20 </h2>" );
 </script>

  若是用json表達的話,通常是如下格式,可是須要將index.ftl的replace()函數改寫:

<script type=」text/javascript」>
replace(
{id:」one」,
 content:」<h1>Part One</h1>
          <h2>你好:John,如今時間時 23:20 </h2>」, 
 css:」[..]「,
js:」[..]「,
…} );
</script>

  這樣瀏覽器就會完成pagelet one的加載,其餘pagelet按此方法進行.它們之間不是順序執行的,而是並行執行,由於在服務端採用並行方式.上面即是整個bigpipe的執行過程了.

4.java實現代碼

  這裏以個人博客園我的首頁作例子,實現一個基於servlet的BigPipe簡單demo.由於是簡單的demo,沒有實現數據庫訪問和頁面的渲染操做,只是作一個簡單的模仿.

4.1 頁面模板  

首先我將我的模塊主頁分爲header(頂部),sideBar(左邊),mainContent(文章列表),footer四個pagelet,寫了一個簡單的頁面框架,代碼以下:

<html xmlns="http://www.w3.org/1999/xhtml" lang="zh-cn">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>JohnZheng - 博客園</title>
<!-- JS 解析函數 -->
<script type="application/javascript">
    function replace(id,content) {
          var pagelet = document.getElementById(id);
          pagelet.innerHTML = content;
}
 </script>
</head>
<body>
 <div style="margin:0px 0px;padding:0px 0px;">
    <!-- header 頭部 -->
    <div id="header" style="height:150px;background-color:#00FFFF;"></div>
    <div style="clear:both"></div>
    <div id="main">
        <!--sideBar 側邊欄容器 -->
        <div id="sideBar" style="width:200px;height:420px;float:left;background-color:#00ff00;"></div>
        <!--mainContent 主體內容容器-->
        <div id="mainContent" style="float:left;width:800px; height:420px;padding-left:10px;"></div>
        <div style="clear:both"></div>
    </div><!--end: main -->
    
    <div style="clear:both"></div>
    <!--footer -->
    <div id="footer" style="background-color:#C0C0C0;height:60px;text-align:center"></div>
</div><!--end: home 自定義的最大容器 -->
<!--</body>-->
<!--</html>-->
index.ftl

  這裏須要注意如下幾點:

  • 模板頁面必須是未封閉的.
  • 由於這裏只是模仿實現,因此我在模板中加入了Js解析函數.如代碼註解處.該解析函數用於將以後的服務端返回的pagelet解析並插入到模板中的對應位置.
  • 本案例是採用freemarker來寫的(其實在該案例中,將後綴名改成html也是能夠的)
  • 雖然此處沒有引入css和js,但若是文件中有css,js,通常是將css放在頭部,而js則是返回全部的pagelet以後再將js返回給前端.

  顯示效果以下圖所示:

   

 4.2 各個Pagelet

   未各個pagelet編寫視圖模板,該視圖模板負責顯示對應的pagelet,規定了pagelet的顯示樣式.因此案例中編寫了header.ftl,sideBar.ftl, mainContent.ftl, footer.ftl. (這裏的文件名不須要和pagelet的div id對應).

   頂部視圖模板 header.ftl:

<div>
<h1><a href="http://www.cnblogs.com/jaylon/">John Zheng</a></h1>
<h2>知止然後定,定然後能靜,靜然後能安,安然後能慮,慮然後能得.</h2>
</div><!--end: blogTitle 博客的標題和副標題 -->

<div id="navigator">    
    博客園 首頁 新聞 新隨筆 聯繫 管理 訂閱
    <div>                
        <!--done-->
        隨筆- 4&nbsp;
        文章- 0&nbsp;
        評論- 2&nbsp;
    </div><!--end: blogStats -->
</div><!--end: navigator 博客導航欄 -->
header.ftl

  左邊視圖模板 sideBar.ftl

<div>
<h3 class="catListTitle">公告</h3>
    暱稱:JohnZheng</br>
    園齡:6個月</br>
    粉絲:4</br>
    關注:1</br>
</div>
    
sideBar.ftl

  文章列表視圖模板 mainContent.ftl

<div class="forFlow">
</br>
<div class="day">
    <div class="dayTitle">
        <a href="http://www.cnblogs.com/jaylon/archive/2015/10/29.html">2015年10月29日</a>                  
    </div>

    
    <div>
        <a  href="http://www.cnblogs.com/jaylon/p/4918914.html">Spring 入門知識點筆記整理</a>
    </div>
    <div ><div >摘要: spring入門學習的筆記整理,主要包括spring概念,容器和aop的入門知識點筆記的整理.<a href="http://www.cnblogs.com/jaylon/p/4918914.html" class="c_b_p_desc_readmore">閱讀全文</a></div></div>
    <div ></div>
    <div >posted @ 2015-10-29 19:59 JohnZheng 閱讀(395) 評論(2)  <a href ="http://i.cnblogs.com/EditPosts.aspx?postid=4918914" rel="nofollow">編輯</a></div>
</div>
</br>

<div >
    <div>
        <a  href="http://www.cnblogs.com/jaylon/archive/2015/10/28.html">2015年10月28日</a>                  
    </div>
    <div >
        <a  href="http://www.cnblogs.com/jaylon/p/4908075.html">spring遠程服務知識梳理</a>
    </div>
    <div>
        <div>摘要: 本文主要是對spring中的幾個遠程調度模型作一個知識梳理.spring所支持的RPC框架能夠分爲兩類,同步調用和異步調用.同步調用如:RMI,Hessian,Burlap,Http Invoker,JAX-WS. RMI採用java序列化,但很難穿過防火牆.Hessian,Burlap都是基於http協議,可以很好的穿過防火牆.但使用了私有的對象序列化機制,Hessian採用二進制傳送數據,而Burlap採用xml,因此Burlap能支持不少語言如python,java等.Http Invoker 是sping基於HTTP和java序列化協議的遠程調用框架,只能用於java程序的通行.Web service(JAX-WS)是鏈接異構系統或異構語言的首選協議,它使用SOAP形式通信,能夠用於任何語言,目前的許多開發工具對其的支持也很好.
          同步通訊有必定的侷限性.因此出現了異步通訊的RPC框架,如lingo和基於sping JMS的RPC框架.<a href="http://www.cnblogs.com/jaylon/p/4908075.html">閱讀全文</a></div>
    </div>
    <div >posted @ 2015-10-28 14:31 JohnZheng 閱讀(424) 評論(0)  <a href ="http://i.cnblogs.com/EditPosts.aspx?postid=4908075" rel="nofollow">編輯</a>
    </div>
</div>

</br>

<div>
    <div >
        <a  href="http://www.cnblogs.com/jaylon/archive/2015/10/24.html">2015年10月24日</a>                  
    </div>

    
    <div >
        <a  href="http://www.cnblogs.com/jaylon/p/4905769.html">Spring Security 入門詳解</a>
    </div>
    <div ><div class="c_b_p_desc">摘要: spring security主要是對spring應用程序的安全控制,它包括web請求級別和方法調度級別的安全保護。本文是主要介紹spring security的基礎知識,對spring security所涉及的全部知識作一個梳理。<a href="http://www.cnblogs.com/jaylon/p/4905769.html" class="c_b_p_desc_readmore">閱讀全文</a></div></div>
    <div></div>
    <div>posted @ 2015-10-24 11:47 JohnZheng 閱讀(331) 評論(0)  <a href ="http://i.cnblogs.com/EditPosts.aspx?postid=4905769" rel="nofollow">編輯</a></div>

</div>

</div><!--end: forFlow -->
mainContent.ftl

  尾部視圖模板 footer.ftl

</br>Copyright &copy;2015 JohnZheng

4.3 BigPipe 案例核心代碼

  如今是構建一個Servlet負責將訪問我的博客首頁的請求用BigPipe方式來加載頁面.這裏我構建了一個BigPipeServlet實現bigPipe的整個流程.當用戶請求BigPipeServlet時

  • 首先將index.ftl頁面模板返回給流瀏覽器,由於只返回一個頁面框架因此用戶很快就能夠看到上面顯示的那張圖的效果.
  • 將index.ftl發送出去之後,BigPipeServlet採用並行的方式獲取數據和渲染界面(這部分工做交給PageletWorker.java)這個線程完成,
  • 若是某個pagelet渲染完成則BigPipeServlet將起flush給瀏覽器.
  • 若是全部的pagelet都flush完成了,那麼封閉頁面,告訴瀏覽器已經完成請求則不用接受服務器端的數據.

  下面是BigPipeServlet.java的代碼:

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setHeader("Content-type", "text/html;charset=UTF-8");  
        response.setCharacterEncoding("UTF-8"); 
        PrintWriter writer = response.getWriter();
        //這裏是獲得一個渲染的頁面框架
        String frameView = Renderer.render("index.ftl");
        //將頁面框架返回給前端
        flush(writer,frameView);
       
    //並行處理pagelet ExecutorService executor
= Executors.newCachedThreadPool(); CompletionService<String> completionService = new ExecutorCompletionService<String>(executor); completionService.submit(new PageletWorker(1500,"header","pagelets/header.ftl")); //處理頭部 completionService.submit(new PageletWorker(2000,"sideBar","pagelets/sideBar.ftl"));//處理左邊信息 completionService.submit(new PageletWorker(4000,"mainContent","pagelets/mainContent.ftl"));//處理文章列表 completionService.submit(new PageletWorker(1000,"footer","pagelets/footer.ftl"));//處理尾部 //若是某個pagelet處理完成則返回給前端 try { for(int i = 0;i < 4; i++){ Future<String> future = completionService.take(); String result = future.get(); flush(writer,result); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } //最後關閉頁腳 closeHtml(writer); } /** * 返回給前端 * @param writer * @param content */ private void flush(PrintWriter writer,String content){ writer.println(content); writer.flush(); } /** * 關閉頁面 * @param writer */ private void closeHtml(PrintWriter writer){ writer.println("</body>"); writer.println("</html>"); writer.flush(); writer.close(); }

  這裏採用了ExecutorCompletionService來負責並行處理pagelet,ExecutorCompletionService是一個包含Executor 和阻塞隊列的類.提交的線程會交給Executor執行,執行的結果放到阻塞隊列中,因此只要從隊列中獲取數據就能夠了,簡化了多線程操做.

  代碼中爲每一個pagelet設置了不一樣的處理時間,這樣在瀏覽器能夠看到不一樣頁面的在不一樣時間顯示.以上代碼中還引用了另外一個類,PageletWorker. 該類主要負責pagelet的業務邏輯處理,獲取所需數據及頁面的渲染.最終將渲染完成的頁面交給BigPipeServlet處理.PageletWorker.java代碼以下:

public class PageletWorker implements Callable<String> {
    //模擬完成業務邏輯的運行時間(pagelet所須要的數據,渲染頁面等的總時間)
    private int runtime;
    //pagelet視圖模板
    private String pageletViewPath;
    private String pageletKey;
    /**
     * 建立一個pagelet 執行器.執行pagelet的業務邏輯和渲染頁面.這裏只是模擬.
     * @param runtime 進行業務處理,數據獲取等的運行時間
   * @param pageletKey 對應html 中div id *
@param pageletViewPath 模板的視圖路徑 */ public PageletWorker(int runtime,String pageletKey, String pageletViewPath) { this.runtime = runtime; this.pageletKey = pageletKey; this.pageletViewPath = pageletViewPath; } public String call() throws Exception { //模仿業務邏輯的處理和相關數據獲取時間 Thread.sleep(runtime); //模仿頁面渲染過程 String result = Renderer.render(pageletViewPath); result = buildJsonResult(result); return result; } /** * 將結果轉化爲json形式  * @param result * @return */ private String buildJsonResult(String result) { StringBuilder sb = new StringBuilder(); sb.append("<script type=\"application/javascript\">") .append("\nreplace(\"") .append(pageletKey) .append("\",\'") .append(result.replaceAll("\n","")).append("\');\n</script>"); return (String) sb.toString(); } }

 

  該類主要邏輯以下:

  • Thread.sleep(runtime) 表示模仿該pagelet的業務處理,數據獲取的時間
  • 將獲取到的數據及該pagelet的視圖模板交給渲染器渲染,獲得渲染後的頁面內容.這裏用Renderer類模仿頁面的渲染過程.
  • 將該頁面內容封裝成json形式返回

  Renderer.java 是模仿獲取渲染過程,這裏主要是讀取pagelet視圖內容.具體代碼以下:  

public class Renderer {
    /**
     * 模範頁面的渲染,這裏主要是獲取對應的頁面信息.
     * @param viewPath
     * @return
     */
    public static String render(String viewPath){
        String absolutePath = Renderer.class.getClassLoader().getResource(viewPath).getPath();
        File file = new File(absolutePath);
        StringBuilder contentBuilder = new StringBuilder();
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(file));
            String str;
            while ((str = br.readLine()) != null) {//使用readLine方法,一次讀一行
                contentBuilder.append(str + "\n");
            }    
        } catch (Exception e) {
            // TODO: handle exception
        }finally{
            if(br != null)
                try {
                    br.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
        }
        return contentBuilder.toString();
    }
}

  在web.xml中配置BigPipeServlet:  

<servlet>
      <servlet-name>bigPipeServlet</servlet-name>
      <display-name>bigPipeServlet</display-name>
      <description></description>
      <servlet-class>org.opensjp.bigpipe.servlet.BigPipeServlet</servlet-class>
  </servlet>
  <servlet-mapping>
      <servlet-name>bigPipeServlet</servlet-name>
      <url-pattern>/bigPipeServlet</url-pattern>
  </servlet-mapping>

  在頁面輸入/bigPipeServlet就能夠看到效果了.先是顯示頁面框架內容,而後陸續的顯示各個pagelet的內容.

  代碼下載地址https://github.com/JohnZhengHub/BigPipe-ServletDemo 

5.實現細節

  若是要實現一個具備良好性能的BigPipe,須要考慮的東西還挺多.能夠從一下幾個方面考慮,提升BigPipe框架的性能.

  1.對搜索引擎的支持.

  這是一個必須考慮的問題,現在是搜索引擎的時代,若是網頁對搜索引擎不友好,或者使搜索引擎很難識別內容,那麼會下降網頁在搜索引擎中的排名,直接 減小網站的訪問次數。在BigPipe 中,頁面的內容都是動態添加的,因此可能會使搜索引擎沒法識別。可是正如前面所說,在服務器端首先要根據user-agent 判斷客戶端是不是搜索引擎的爬蟲,若是是的話,則轉化爲原有的模式,而不是動態添加。這樣就解決了對搜索引擎的不友好。

  2.將資源文件進行壓縮,提升傳輸速度.能夠考慮使用G-zip對css和js文件進行壓縮

  3.對js文件進行精簡:對js 文件進行精簡,能夠從代碼中移除沒必要要的字符,註釋以及空行以減少js 文件的大小,從而改善加載的頁面的時間。精簡js 腳本的工具可使用JSMin,使用精簡後的腳本的大小會減小20%左右。這也是一個很大的提高。

  4.將樣式表放在頂部

  將html 內容所需的css 文件放在首部加載是很是重要的。若是放在頁面尾部,雖然會使頁面內容更快的加載(由於將加載css 文件的時間放在最後,從而使頁面內容先顯示出來),可是這樣的內容是沒有使用樣式表的,在css 文件加載進來後,瀏覽器會對其使用樣式表,即再次改變頁面的內容和樣式,稱之爲「無樣式內容的閃爍」,這對於用戶來講固然是不友好的。實現的時候將css 文件放在<head>標籤中便可。

   5.將js放在底部

  支持頁面動態內容的Js 腳本對於頁面的加載並無什麼做用,把它放在頂部加載只會使頁面更慢的加載,這點和前面的提到的css 文件恰好相反,因此能夠將它放在頁尾加載。

相關文章
相關標籤/搜索