一.上文回顧
上回我們主要從圖片的合并、壓縮等方面介紹前端性能優化問題(詳見Java Web 前端高性能
優化(一))
本次我們主要從圖像BASE64 編碼、GZIP壓縮、懶加載與預加載以及?OneAPM?Browser
Insight 的定位分析功能四個方面介紹前端優化方法
二.圖像的 BASE64 編碼
不管如何,圖片的下載始終都要向服務器發出請求,要是圖片的下載不用向服務器發出請求,
而可以隨著 HTML 的下載同時下載到本地那就太好了。而目前,瀏覽器已經支持了該特性,
我們可以將圖片數據編碼成 BASE64 的字符串,使用該字符串代替圖像地址。假設用 S代表
這個 BASE64 字符串,那么就可以使用<img src="data:image/png;base64,S">
?來顯示
這個圖像??梢钥闯觯瑘D像的數據包含在了 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 定位分析
作為一個網站的前端運維人員或者優化人員,大多數情況下并不一定要注重每一位用戶的訪問
情況,只要大部分用戶訪問網站的時候處于一個滿意的程度就可以了?,F在大多數前端性能優
化工具往往注重的是某個時間段內的頁面平均響應時間,這就造成可能因為某個用戶偶然性的
網絡卡頓而延長整個時間段內的頁面加載時間。
前一段時間發現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<=2; i++) { imageObj.src=images[i]; if (imageObj.complete) { // 如果圖片已經存在于瀏覽器緩存,直接調用回調函數 callback.call(imageObj); } else { imageObj.onload = function () {// 圖片下載完畢時異步調用 callback 函數 callback.call(imageObj);// 將回調函數的 this 替換為 Image 對象 }; } } } function callback() { alert(this.src + “已經加載完畢 , 可以在這里繼續預加載下一組圖片”); }
上面的代碼,首先定義了 Image 對象,并且聲明了需要預加載的圖像數組,然后逐一的開始
加載(.src=images[i])。如果已經在緩存里,則不做其他處理;如果不在緩存,監聽 onload
事件,它會在圖片加載完畢時調用。
而懶加載則是在用戶需要的時候再加載。當一個網頁中可能同時有上百張圖片,而大部分情況
下,用戶只看其中的一部分,如果同時顯示上百張,則浪費了大量帶寬資源,因此可以當用戶
往下拉動滾動條時,才去請求下載被查看的圖像,這個原理與 word 的顯示策略非常類似。
在 JavaScript 中,它的基本原理是首先要有一個容器對象,容器里面是 img 元素集合。用隱
藏或替換等方法,停止 img 的加載,也就是停止它去下載圖像。然后歷遍 img 元素,當元素
在加載范圍內,再進行加載(也就是顯示或插入 img 標簽)。加載范圍一般是容器的視框范
圍,即瀏覽者的視覺范圍內。當容器滾動或大小改變時,再重新歷遍元素判斷。如此重復,直
到所有元素都加載后就完成。當然對于開發來講,選擇已有的成熟組件,并不失為一個上策
,Lazy Load Plugin for jQuery 是基于 JQuery 的懶加載組件,它有自己的官方網站4。這是一
個不錯的免費插件??梢詭椭绦騿T快速的開發懶加載應用。
小結
Java Web 前端高性能優化(一)、(二)總結了前端性能問題定位以及圖片優化的幾種方式,
將它們歸結起來,在讀者需要的時候,可以查看本文的內容,相信按照本文的方法,可以輔助
讀者進行前端的高性能優化。
注:本文轉載自 IBM 社區,由 OneAPM 產品運營編輯整理,原文鏈接為:
http://www.ibm.com/developerworks/cn/java/j-lo-javawebhiperf1/#icomments