Java Web 前端高效能優化(二)

OneAPM官方技術部落格發表於2015-12-09
一.上文回顧

上回我們主要從圖片的合併、壓縮等方面介紹前端效能優化問題(詳見Java Web 前端高效能優化(一)

本次我們主要從影像BASE64 編碼、GZIP壓縮、懶載入與預載入以及 OneAPM Browser Insight 的定位分析功能四個方面介紹前端優化方法

二.影像的 BASE64 編碼

不管如何,圖片的下載始終都要向伺服器發出請求,要是圖片的下載不用向伺服器發出請求,而可以隨著 HTML 的下載同時下載到本地那就太好了。而目前,瀏覽器已經支援了該特性,我們可以將圖片資料編碼成 BASE64 的字串,使用該字串代替影像地址。

假設用 S代表這個 BASE64 字串,那麼就可以使用<img src="data:image/png;base64,S"> 來顯示這個影像。可以看出,影像的資料包含在了 HTML 程式碼裡,無需再次訪問伺服器。那麼影像要如何編碼成 BASE64 字串呢?

可以使用 線上的工具---“Base64 Online”,這個工具可以上傳圖片將圖片轉換為 BASE64 字串。當然,如果讀者有興趣,完全可以自己實現一個 BASE64 編碼工具,比如使用 Java 開發,它的程式碼就如清單 1 所示。

清單 1. BASE64 的 Java 程式碼

 public static String getPicBASE64(String picPath) {   
        String content = null;   
        try {   
            FileInputStream fis = new FileInputStream(picPath);   
            byte[] bytes = new byte[fis.available()];   
            fis.read(bytes);   
            content = new sun.misc.BASE64Encoder().encode(bytes); // 具體的編碼方法   
            fis.close();     
        } catch (Exception e) {   
            e.printStackTrace();   
        }   
        return content;   
 }

本文編碼了一個影像,並且將編碼獲得的 BASE64 字串,寫到了 HTML 之中,如下清單 2 所示。

清單 2. 嵌入 BASE64 的測試 HTML 程式碼 <html> <body> <img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAeQAAAB8BAMAAABKwt5QAAAAA3NCSVQICAjb4U/gAAAAGFBMVEX/ ……(省略了大部分編碼)… BJRU5ErkJggg=="> </body> </html> 由於圖片資料包含在了 BASE64 字串中,因此無需向伺服器請求影像資料,結果顯示如下圖所示。

圖 1. BASE64 顯示影像

前端高效能資源優化(二)

然而這種策略並不能濫用,它適用的情況是瀏覽器連線伺服器的時間 > 圖片下載時間,也就是發起連線的代價要大於圖片下載,那麼這個時候將圖片編碼為 BASE64 字串,就可以避免連線的建立,提高效率。如果圖片較大的話,使用 BASE64 編碼雖然可以避免連線建立,但是相對於影像下載,請求的建立只佔很小的比例,如果用 BASE64,對於動態網頁來說影像快取就會失效(靜態網頁可以快取),而且 BASE64 字串的總大小要大於純圖片的大小,這樣一算就非常不合適了。

因此,如果你的頁面已經靜態化,影像又不是非常大,可以嘗試 BASE64 編碼,客戶端會將網頁內容和圖片的 BASE64 編碼一起快取;而如果你的頁面是動態頁面,影像還較大,每次都要下載 BASE64 字串,那麼就不能用 BASE64 編碼影像,而正常引用影像,從而使用到瀏覽器的影像快取,提高下載速度。從現實我們接觸的角度看,如一些線上 HTML 編輯器,裡面的小圖示,如笑臉等,都使用到了 BASE64 編碼,因為它們非常小,數量多,BASE64 可以幫助網頁減少圖示的請求數,提高效率。

三.Browser Insight 定位分析

作為一個網站的前端運維人員或者優化人員,大多數情況下並不一定要注重每一位使用者的訪問情況,只要大部分使用者訪問網站的時候處於一個滿意的程度就可以了。

現在大多數前端效能優化工具往往注重的是某個時間段內的頁面平均響應時間,這就造成可能因為某個使用者偶然性的網路卡頓而延長整個時間段內的頁面載入時間。

前一段時間發現 OneAPM 的Browser Insight 推出了定位分析功能,可以從響應時間分佈來檢視使用者的整體響應分佈,並可以針對不同時間分佈內的使用者確定影響其響應時間的因素。

圖 2.Browser Insight 定位分析

前端高效能資源優化(二)

這個功能確實對於前段優化人員來說非常實用,並且它的維度還很豐富。

四.GZIP 壓縮

為了減少傳輸的資料,壓縮是一個不錯的選擇,而 HTTP 協議支援 GZIP 的壓縮格式,伺服器響應的報頭包含 Content-Encoding: gzip,它告訴瀏覽器,這個響應的返回資料,已經壓縮成 GZIP 格式,瀏覽器獲得資料後要進行解壓縮操作。這在一定程度可以減少伺服器傳輸的資料,提高系統效能。

那麼如何給伺服器響應新增 Content-Encoding: gzip 報頭,同時壓縮響應資料呢?

如果你用的是 Tomcat 伺服器,開啟 $tomcat_home$/conf/server.xml 檔案,對 Connector 進行配置,配置如清單 3 所示。

清單 3. TOMCAT 配置清單

<Connector port ="80" maxHttpHeaderSize ="8192" maxThreads ="150" minSpareThreads ="25" maxSpareThreads ="75" enableLookups ="false" redirectPort ="8443" acceptCount ="100" connectionTimeout ="20000" disableUploadTimeout ="true" URIEncoding ="utf-8"
compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml" />

我們為 Connector 新增了如下幾個屬性,他們意義分別是:

compression="on" 開啟壓縮功能

compressionMinSize="2048"啟用壓縮的輸出內容大小,這裡面預設為 2KB

noCompressionUserAgents="gozilla, traviata" 對於以下的瀏覽器,不啟用壓縮

compressableMimeType="text/html,text/xml, image/png" 壓縮型別

有時候,我們無法配置 server.xml,比如如果我們只是租用了別人的空間,但是它並沒有啟用GZIP,那麼我們就要使用程式啟用 GZIP 功能。我們將需要壓縮的檔案,放到指定的資料夾,使用一個過濾器,過濾對這個資料夾裡檔案的請求。

清單 4. 自定義 Filter 壓縮 GZIP

 // 監視對 gzipCategory 資料夾的請求
 @WebFilter(urlPatterns = { "/gzipCategory/*" }) 
 public class GZIPFilter implements Filter { 

 @Override 
 public void doFilter(ServletRequest request, ServletResponse response, 
 FilterChain chain) throws IOException, ServletException { 
 String parameter = request.getParameter("gzip"); 
 // 判斷是否包含了 Accept-Encoding 請求頭部
 HttpServletRequest s = (HttpServletRequest)request; 
 String header = s.getHeader("Accept-Encoding"); 
 //"1".equals(parameter) 只是為了控制,如果傳入 gzip=1,才執行壓縮,目的是測試用
 if ("1".equals(parameter) && header != null && header.toLowerCase().contains("gzip")) { 
 HttpServletResponse resp = (HttpServletResponse) response; 
 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 

 HttpServletResponseWrapper hsrw = new HttpServletResponseWrapper( 
 resp) { 

 @Override 
 public PrintWriter getWriter() throws IOException { 
 return new PrintWriter(new OutputStreamWriter(buffer, 
 getCharacterEncoding())); 
 } 

 @Override 
 public ServletOutputStream getOutputStream() throws IOException { 
 return new ServletOutputStream() { 

 @Override 
 public void write(int b) throws IOException { 
 buffer.write(b); 
 } 
 }; 
 } 

 }; 

 chain.doFilter(request, hsrw); 
 byte[] gzipData = gzip(buffer.toByteArray()); 
 resp.addHeader("Content-Encoding", "gzip"); 
 resp.setContentLength(gzipData.length); 
 ServletOutputStream output = response.getOutputStream(); 
 output.write(gzipData); 
 output.flush(); 
 } else { 
 chain.doFilter(request, response); 
 } 
 } 
 // 用 GZIP 壓縮位元組陣列
 private byte[] gzip(byte[] data) { 
 ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(10240); 
 GZIPOutputStream output = null; 
 try { 
 output = new GZIPOutputStream(byteOutput); 
 output.write(data); 
 } catch (IOException e) { 
 } finally { 
 try { 
 output.close(); 
 } catch (IOException e) { 
 } 
 } 
 return byteOutput.toByteArray(); 
 } 
……
 }

該程式的主體思想是:在響應流寫回之前,對響應的位元組資料進行 GZIP 壓縮。

因為並不是所有的瀏覽器都支援 GZIP 解壓縮,如果瀏覽器支援 GZIP 解壓縮,會在請求報頭的 Accept-Encoding 裡包含 gzip。這是告訴伺服器瀏覽器支援 GZIP 解壓縮,因此如果用程式控制壓縮,為了保險起見,還需要判斷瀏覽器是否傳送 accept-encoding: gzip 報頭,如果包含了該報頭,才執行壓縮。為了驗證壓縮前後的情況,使用 Firebug 監控請求和響應報頭。

清單 5. 壓縮前請求

 GET /testProject/gzipCategory/test.html HTTP/1.1 
 Accept: */* 
 Accept-Language: zh-cn 
 Accept-Encoding: gzip, deflate 
 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) 
 Host: localhost:9090 
 Connection: Keep-Alive

清單 6. 不壓縮的響應

 HTTP/1.1 200 OK 
 Server: Apache-Coyote/1.1 
 ETag: W/"5060-1242444154000"
 Last-Modified: Sat, 16 May 2009 03:22:34 GMT 
 Content-Type: text/html 
 Content-Length: 5060
 Date: Mon, 18 May 2009 12:29:49 GMT

清單 7. 壓縮後的響應

 HTTP/1.1 200 OK 
 Server: Apache-Coyote/1.1 
 ETag: W/"5060-1242444154000"
 Last-Modified: Sat, 16 May 2009 03:22:34 GMT 
 Content-Encoding: gzip
 Content-Type: text/html 
 Content-Length: 837
 Date: Mon, 18 May 2009 12:27:33 GMT

可以看到,壓縮後的資料比壓縮前資料小了很多。壓縮後的響應報頭包含 Content-Encoding: gzip。

同時 Content-Length 包含了返回資料的大小。GZIP 壓縮是一個重要的功能,前面提到的是對單一伺服器的壓縮優化,在高併發的情況,多個 Tomcat 伺服器之前,需要採用反向代理的技術,提高併發度,而目前比較火的反向代理是 Nginx(這在後續的文章會進行詳細的介紹)。

對 Nginx 的 HTTP 配置部分裡增加如下配置。

清單 8. Nginx 的 GZIP 配置

 gzip  on; 
 gzip_min_length  1000; 
 gzip_buffers     4 8k; 
 gzip_types       text/plain application/x-javascript text/css text/html application/xml;

由於 Nginx 具有更高的效能,利用該配置可以更好的提高效能。在高效能伺服器上該配置將非常有用。

五.懶載入與預載入

預載入和懶載入,是一種改善使用者體驗的策略,它實際上並不能提高程式效能,但是卻可以明顯改善使用者體驗或減輕伺服器壓力。

預載入原理是在使用者檢視一張圖片時,就將下一張圖片先下載到本地,而當使用者真正訪問下一張圖片時,由於本地快取的原因,無需從伺服器端下載,從而達到提高使用者體驗的目的。為了實現預載入,我們可以實現如下的一個函式。

清單 9. 預載入函式

 function  preload(callback) {  
 var imageObj = new Image(); 
 images = new Array(); 
 images[0]="pre_image1.jpg"; 
 images[1]=" pre_image2.jpg"; 
 images[2]=" pre_image3.jpg"; 
 for(var i=0; i

上面的程式碼,首先定義了 Image 物件,並且宣告瞭需要預載入的影像陣列,然後逐一的開始載入(.src=images[i])。如果已經在快取裡,則不做其他處理;如果不在快取,監聽 onload 事件,它會在圖片載入完畢時呼叫。

而懶載入則是在使用者需要的時候再載入。當一個網頁中可能同時有上百張圖片,而大部分情況下,使用者只看其中的一部分,如果同時顯示上百張,則浪費了大量頻寬資源,因此可以當使用者往下拉動滾動條時,才去請求下載被檢視的影像,這個原理與 word 的顯示策略非常類似。

在 JavaScript 中,它的基本原理是首先要有一個容器物件,容器裡面是 img 元素集合。用隱藏或替換等方法,停止 img 的載入,也就是停止它去下載影像。然後歷遍 img 元素,當元素在載入範圍內,再進行載入(也就是顯示或插入 img 標籤)。

載入範圍一般是容器的視框範圍,即瀏覽者的視覺範圍內。當容器滾動或大小改變時,再重新曆遍元素判斷。如此重複,直到所有元素都載入後就完成。當然對於開發來講,選擇已有的成熟元件,並不失為一個上策,Lazy Load Plugin for jQuery 是基於 JQuery 的懶載入元件,它有自己的官方網站

這是一個不錯的免費外掛。可以幫助程式設計師快速的開發懶載入應用。

小結

Java Web 前端高效能優化(一)(二)總結了前端效能問題定位以及圖片優化的幾種方式,將它們歸結起來,在讀者需要的時候,可以檢視本文的內容,相信按照本文的方法,可以輔助讀者進行前端效能優化

:本文轉載自 IBM 社群,由 OneAPM 產品運營編輯整理,原文連結為: http://www.ibm.com/developerworks/cn/java/j-lo-javawebhiperf1/#icomments

相關文章