Java Web 高效能開發,第 1 部分: 前端的高效能
原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-javawebhiperf1/
引言
前端的高效能部分,主要是指減少請求數、減少傳輸的資料以及提高使用者體驗,在這個部分,圖片的優化顯得至關重要。許多網站的美化,都是靠絢麗的圖片達到的,圖片恰恰是佔用頻寬的元凶。每個 img 標籤,瀏覽器都會試圖發起一個下載請求。本文就詳細介紹了圖片優化的幾種方式,介紹了使用的工具以及優化後的結果。
圖片壓縮
減少圖片的大小,可以明顯的提高效能,而對於已有圖片,要想減少圖片的大小,只能改變圖片的格式,這裡推薦的是 PNG8 的格式,它可以在基本保持清晰度的情況下,減少圖片的大小。知道這個原理以後,可以用 Windows 的畫圖工具、以及 PhotoShop 工具逐個的改變。但是這樣做的缺點是單張處理,效率太慢。本文推薦一個線上轉換工具 Smush.it,可以批量的進行壓縮與轉換。它的地址是:www.smushit.com/ysmush.it。開啟後效果如下圖所示。
圖 1. Yahoo 提供的線上壓縮工具
我們上傳了一張大小為 3790K 的圖片,待線上程式處理完畢後,點選 Download Smushed Images 下載檢視結果。下載介面如下圖所示。
圖 2. 壓縮後的結果
開啟下載下來的壓縮包,檢視結果可以看到,圖片從 3790 減少到了 3344,就如下圖所示。對於大批量的圖片網站,這個方法會幫助快速實現批量圖片壓縮。
圖 3. 壓縮後的結果
影象合併實現 CSS Sprites
CSS Sprites 是一個吸引人的技術,它其實就是把網頁中一些背景圖片整合到一張圖片檔案中,再利用 CSS 的“background-image”,“background- repeat”,“background-position”的組合進行背景定位,background-position 可以用數字能精確的定位出背景圖片的位置。利用 CSS Sprites 能很好地減少網頁的 HTTP 請求,從而大大的提高了頁面的效能,這也是 CSS Sprites 最大的優點,也是其被廣泛傳播和應用的主要原因。CSS Sprites 能減少圖片的位元組,由於影象合併後基本資訊不用重複,那麼多張圖片合併成 1 張圖片的位元組往往總是小於這些圖片的位元組總和。同時 CSS Sprites 解決了網頁設計師在圖片命名上的困擾,只需對一張集合的圖片上命名就可以了,不需要對每一個小元素進行命名,從而提高了網頁的製作效率。更換風格方便,只需要在一張或少張圖片上修改圖片的顏色或樣式,整個網頁的風格就可以改變。維護起來更加方便。同時,由於將圖片合併到一張圖片,因此圖片的請求數就被縮減到 1 個。其他的請求都可以用到本地快取,不需要訪問伺服器。下圖是一個合併以後的圖片。它將很多小圖示都拼到了一起。
圖 4. 合併後的圖片
這裡介紹一個小工具 ---“CSS Sprites 樣式生成工具 2.0”,可以從 這裡下載。這是一個簡單免費的小工具,用該工具開啟上面的圖片,選中圖片中的某塊。如下圖的“綠色大拇指”部分,工具會計算出這個部分的長、寬、距離左上角的距離。勾選複製類名、複製寬、複製高,再點選“複製當前樣式”按鈕。這樣生成的樣式會被複制到剪下板上。
圖 5. 小工具的使用
生成的 CSS 程式碼如清單 1 所示。
清單 1. 小工具生成的 CSS 程式碼
.div_6148{width:18px;height:20px;background-position:-17px -209px;}
將這段程式碼運用在網頁上,它的程式碼如下清單所示。
清單 2. 測試 CSS Sprites 程式碼
<html> <head> <style> .div_6148 { width:18px; height:20px; background-image:url(css-sprites-source.gif); background-position:-17px -209px; } </style> </head> <body> <div class="div_6148"></div> </body> </html>
開啟測試網頁顯示結果如下圖所示。
圖 6. 測試網頁效果
可以看到,網頁只顯示工具選擇的“綠色大拇指”部分,這樣的程式碼可以運用在網頁的多個部分,而圖片只需要下載一次,這就是該技術的最大優勢,減少了因為小圖片引起的多個請求。
多域名請求
有時候,圖片資料太多,一些公司的解決方法是將圖片資料分到多個域名的伺服器上,這在一方面是將伺服器的請求壓力分到多個硬體伺服器上。另一方面,是利用了瀏覽器的特性。一般來說,瀏覽器對於相同域名的圖片,最多用 2-4 個執行緒並行下載。不同瀏覽器的併發下載數,都是不同的,併發數如下清單所示。
清單 3. 各瀏覽器的併發下載數
Browsers HTTP/1.1 HTTP/1.0 IE6,7 2 4 IE8 6 6 FireFox 2 2 8 FireFox 3 6 6 Safari 3,4 4 4 Chrome 1,2 6 ? Chrome 3 4 4 Opera 9.63,10.00alpha 4 4
而相同域名的其他圖片,則要等到其他圖片下載完後才會開始下載。 這裡我做了一個測試,選擇了多個相同域名的圖片在同一網頁上。程式碼如清單 5 所示。
清單 4. 單域名的多圖片下載
<html> <body> <img src="http://img1.gtimg.com/news/pics/hv1/123/231/804/52339128.jpg"><br> <img src="http://img1.gtimg.com/news/pics/hv1/87/235/804/52340112.jpg"><br> <img src="http://img1.gtimg.com/finance/pics/hv1/41/119/804/52310486.jpg"><br> <img src="http://img1.gtimg.com/sports/pics/hv1/246/198/804/52330836.jpg"><br> <img src="http://img1.gtimg.com/ent/pics/hv1/101/54/805/52358996.jpg"><br> <img src="http://img1.gtimg.com/blog/pics/hv1/169/226/804/52337899.jpg"> </body> </html>
接下來,使用 FireFox 的 Firebug 外掛監控網路。結果如下圖所示。
圖 7. 單域名多圖片的監控效果
可以看到,相同域名的多張圖片,它們下載的起始點是存在延遲的。它們並不是並行下載。當我們將其中的 3 張圖片換成別的域名圖片。如清單 6 所示。
清單 5. 多域名多圖片下載
<html> <body> <img src="http://img1.gtimg.com/news/pics/hv1/123/231/804/52339128.jpg"><br> <img src="http://img1.gtimg.com/news/pics/hv1/87/235/804/52340112.jpg"><br> <img src="http://img1.gtimg.com/finance/pics/hv1/41/119/804/52310486.jpg"><br> <img src="http://i0.itc.cn/20110624/64a_2ee7d710_2ec6_b38d_b678_dc3af28392be_1.jpg"><br> <img src="http://i0.itc.cn/20110624/3b0_643eaea5_1233_b543_82b7_9c7273c7f97c_1.jpg"><br> <img src="http://i0.itc.cn/20110623/962_fa6e8a78_625a_1234_147f_3a627fe17033_1.jpg"> </body> </html>
再次檢視網路監控,可以看到,這些圖片是並行下載的。
圖 8. 多域名多圖片測試結果
多域名的下載固然很好,但是太多域名並不太好,一般在 2-3 個域名下載就差不多。
影象的 BASE64 編碼
不管如何,圖片的下載始終都要向伺服器發出請求,要是圖片的下載不用向伺服器發出請求,而可以隨著 HTML 的下載同時下載到本地那就太好了。而目前,瀏覽器已經支援了該特性,我們可以將圖片資料編碼成 BASE64 的字串,使用該字串代替影象地址。假設用S代表這個 BASE64 字串,那麼就可以使用 <img src="data:image/png;base64,S"> 來顯示這個影象。可以看出,影象的資料包含在了 HTML 程式碼裡,無需再次訪問伺服器。那麼影象要如何編碼成 BASE64 字串呢?可以使用線上的工具---“Base64 Online”,這個工具可以上傳圖片將圖片轉換為 BASE64 字串。當然,如果讀者有興趣,完全可以自己實現一個 BASE64 編碼工具,比如使用 Java 開發,它的程式碼就如清單 7 所示。
清單 6. 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 之中,如下清單 8 所示。
清單 7. 嵌入 BASE64 的測試 HTML 程式碼
<html> <body> <img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAeQAAAB8BAMAAABKwt5QAAAAA3NCSVQICAjb4U/gAAAAGFBMVEX/ ……(省略了大部分編碼)… BJRU5ErkJggg=="> </body> </html>
由於圖片資料包含在了 BASE64 字串中,因此無需向伺服器請求影象資料,結果顯示如下圖所示。
圖 9. BASE64 顯示影象
然而這種策略並不能濫用,它適用的情況是瀏覽器連線伺服器的時間 > 圖片下載時間,也就是發起連線的代價要大於圖片下載,那麼這個時候將圖片編碼為 BASE64 字串,就可以避免連線的建立,提高效率。如果圖片較大的話,使用 BASE64 編碼雖然可以避免連線建立,但是相對於影象下載,請求的建立只佔很小的比例,如果用 BASE64,對於動態網頁來說影象快取就會失效(靜態網頁可以快取),而且 BASE64 字串的總大小要大於純圖片的大小,這樣一算就非常不合適了。因此,如果你的頁面已經靜態化,影象又不是非常大,可以嘗試 BASE64 編碼,客戶端會將網頁內容和圖片的 BASE64 編碼一起快取;而如果你的頁面是動態頁面,影象還較大,每次都要下載 BASE64 字串,那麼就不能用 BASE64 編碼影象,而正常引用影象,從而使用到瀏覽器的影象快取,提高下載速度。從現實我們接觸的角度看,如一些線上 HTML 編輯器,裡面的小圖示,如笑臉等,都使用到了 BASE64 編碼,因為它們非常小,數量多,BASE64 可以幫助網頁減少圖示的請求數,提高效率。
GZIP 壓縮
為了減少傳輸的資料,壓縮是一個不錯的選擇,而 HTTP 協議支援 GZIP 的壓縮格式,伺服器響應的報頭包含 Content-Encoding: gzip,它告訴瀏覽器,這個響應的返回資料,已經壓縮成 GZIP 格式,瀏覽器獲得資料後要進行解壓縮操作。這在一定程度可以減少伺服器傳輸的資料,提高系統性能。那麼如何給伺服器響應新增 Content-Encoding: gzip 報頭,同時壓縮響應資料呢?如果你用的是 Tomcat 伺服器,開啟 $tomcat_home$/conf/server.xml 檔案,對 Connector 進行配置,配置如清單 9 所示。
清單 8. 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 功能。我們將需要壓縮的檔案,放到指定的資料夾,使用一個過濾器,過濾對這個資料夾裡檔案的請求。
清單 9. 自定義 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 監控請求和響應報頭。
清單 10. 壓縮前請求
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
清單 11. 不壓縮的響應
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
清單 12. 壓縮後的響應
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 配置部分裡增加如下配置。
清單 13. 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 具有更高的效能,利用該配置可以更好的提高效能。在高效能伺服器上該配置將非常有用。
懶載入與預載入
預載入和懶載入,是一種改善使用者體驗的策略,它實際上並不能提高程式效能,但是卻可以明顯改善使用者體驗或減輕伺服器壓力。
預載入原理是在使用者檢視一張圖片時,就將下一張圖片先下載到本地,而當用戶真正訪問下一張圖片時,由於本地快取的原因,無需從伺服器端下載,從而達到提高使用者體驗的目的。為了實現預載入,我們可以實現如下的一個函式。
清單 14. 預載入函式
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 的懶載入元件,它有自己的 官方網站。這是一個不錯的免費外掛。可以幫助程式設計師快速的開發懶載入應用。