效能優化的十二個方面

weixin_34236869發表於2016-03-04

原文地址:https://auth0.com/blog/2016/02/22/12-steps-to-a-faster-web-app/
技術文章首發地址:http://pinggod.com/
歡迎關注微博@ping4god,交流技術

現在,web app 日益重視使用者的互動體驗,瞭解效能優化的方式則可以有效提高使用者體驗。閱讀和實踐下面的效能優化技巧,可以幫你改善應用的流暢度、渲染時間和其他方面的效能表現。

概述

對 web app 進行效能優化是一份冗雜沉重的工作,這不僅是因為構建一個 web app 需要前後端協作,而且需要多方面的技術棧:資料庫、後端、前端,需要執行在多種平臺:iOS,安卓,Chrome,Firefox,Edge。這太複雜了!不過,還是有一些歷經實踐的通用方式可以用來優化 web app 的效能。在接下來的小節中,我們將逐步介紹相關的細節。

一份來自 Bing 的研究表明,頁面載入時間每增加 10ms,每年就會減少 $250k 的收入。
———— Rob Trace 和 David Walp,來自微軟的高階產品經理

過早優化

效能優化的難點在於找出開發中值得優化的地方。Donald Knuth 說過一句經典的話:“過早的優化是一切罪惡的根源”。這句話背後的意思是說:花費大量時間改善 1% 的效能毫無意義。同時,某些優化方案反倒影響了可讀性或可維護性,甚至引入了新的問題。換言之,效能優化不應該被視為“榨乾應用程式效能的方法”,而應該視為“對效能和收益的平衡性所進行的探索”。在踐行以下優化技巧時一定要牢記,盲目優化會影響生產效率,甚至得不償失。最好的方式是使用分析工具來查詢效能瓶頸,並在效能優化和開發效率、可維護性等方面保持平衡。

開發者浪費了大量的時間去思考或者擔心程式的執行速度,但實際上從除錯和後期維護的角度看,這些優化措施往往會帶來嚴重的負面影響。我們應該著重 97% 的執行表現:過早的效能優化是一切罪惡的根源。當然,我們也不應該放棄 3% 的痛點。
———— Donald Knuth

1. 檔案壓縮和模組打包

JavaScript 通常是直接使用原始碼的方式分發的,而原始碼解析起來往往要慢於位元組碼。對於小指令碼來說,兩者解析的速度並不大,但對於大的應用程式來說,則會明顯影響應用程式的啟動速度。解決這一痛點,正是 WebAssembly 的出發點之一,它將大幅改善程式的啟動速度。檔案壓縮是剔除檔案中無用字元的流程,雖然處理後的程式碼喪失了可讀性,但提高了瀏覽器的解析速度。

另一方面,模組打包可以將不同的指令碼合併為一個指令碼,從而降低 HTTP 請求,減少資源載入時間。通常來說,這種工作都會交給相應的工具來處理,比如 Webpack

function insert(i) {
    document.write("Sample " + i);
}

for(var i = 0; i < 30; ++i) {
    insert(i);
}

壓縮之後:

!function(r){function t(o){if(e[o])return e[o].exports;var n=e[o]={exports:{},id:o,loaded:!1};return r[o].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t){function e(r){document.write("Sample "+r)}for(var o=0;30>o;++o)e(o)}]);
//# sourceMappingURL=bundle.min.js.map

深度打包

使用 Webpack,我們也可以壓縮 CSS 和合並圖片,進一步改善程式的啟動速度。更多有關 Webpack 的資訊請參考官方文件

2. 按需載入

按需載入資源或者說惰性載入資源(特別是圖片)對優化 web app 的效能有很大幫助。對於圖片較多的頁面,使用惰性載入通常有以下三點好處:

  • 減少併發請求,緩解伺服器壓力,提高載入速度
  • 減少瀏覽器的記憶體佔用率
  • 降低伺服器的負載

圖片或其他資源惰性載入的方案一般是,在程式啟動時載入首屏資源,在頁面滾動時持續載入即將進入視口的資源。由於這種方法往往需要與頁面結構和開發方式相協調,所以常常使用現有的外掛和擴充套件來實現惰性載入。舉例來說,react-lazy-load 是一個基於 React 的圖片惰性載入外掛:

const MyComponent = () => (
  <div>
    Scroll to load images.
    <div className="filler" />
    <LazyLoad height={762} offsetVertical={300}>
      ![](http://upload-images.jianshu.io/upload_images/324643-60bf91dd83e8c6b0.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    </LazyLoad>
    (...)

一個典型的按需載入例項就是谷歌的圖片搜尋工具,點選這一連結並滾動頁面,開啟開發者工具注意資源的載入時間。

3. array-ids

如果你正在使用 React / Ember / Angular 或者其他操作 DOM 的第三方庫,那麼使用 array-ids(或者是 Angular 1.x 中的 track-by 特性)可以有效提高頁面效能,對動態網站的效能優化尤為突出。從最新的基準測試中我們也可以看出其中的優勢:More Benchmarks: Virtual DOM vs Angular 1 & 2 vs Mithril.js vs cito.js vs The Rest (Updated and Improved!)

https://cdn.auth0.com/blog/newdombenchs2/usedheap.svg

其背後的核心概念就是儘可能多地重複利用現有節點。Array-ids 便於 DOM 操作引擎根據獲取到的 DOM 節點與真實的節點相匹配。如果沒有 array-id 或者 track-by,大多數第三方庫都會簡單粗暴的刪除節點然後再建立節點,這會嚴重影響程式的執行速度。

4. 快取

快取常用來儲存頻繁呼叫的資料,當快取後的資料再次被呼叫時,就可以由快取直接提供資料,提高資料的響應速度。通常來說,一個 web app 都是由多個元件構成的,在這些元件中都能發現快取的影子。比如動態內容伺服器和客戶端之間使用的快取,通過減少通用請求降低伺服器負載,可以改善頁面的響應時間;比如程式碼中的快取處理,可以優化某些通用的指令碼訪問模式。此外,還有資料庫快取和長程式快取等。

簡而言之,快取是改善應用程式響應速度和降低 CPU 負載的有效方式。在一個開發體系中,最難的不是如何使用快取,而是找出哪裡適合使用快取。對於這一問題,我還是建議使用事件分析工具(profiler):找出效能瓶頸,檢測快取是否成功,測試快取是否容易失效……這些問題都需要歷經實踐才能得出有效的結論。

使用快取可以優化資源載入,比如,使用 basket.js 利用本地儲存快取應用的指令碼,在第二次呼叫資源時可以迅速從本地儲存中獲得相應的資源。

Amazon CloudFront 是現在比較流行的一項快取服務。CloudFront 的工作機制類似內容分發網路(CDN),可以為動態內容設定快取。

5. HTTP/2

目前,已經有越來越多的瀏覽器支援 HTTP/2。HTTP/2 的優勢在於它與伺服器的併發連線,比如,如果需要載入的小型資源(前提是你不對資源進行打包)比較多,HTTP/2 在響應時間和效能上都要遠遠優勝於 HTTP/1。你可以點選 Akamai 的 HTTP/2 示例 檢視兩者的區別。

https://cdn.auth0.com/blog/fasterweb/http2demo.png

6. 效能剖析

效能剖析是應用程式進行效能優化的重要步驟。如上文所說,盲目地優化應用程式往往會降低生產力、產生新的痛點且難以維護。效能剖析的作用就是要找出應用程式中潛在的風險區域。

對 web 應用程式來說,響應速度是一個非常重要的衡量指標,所以開發者都會盡可能地去提高資源的載入速度和頁面的渲染速度。Chrome 瀏覽器提供了一系列優秀的效能剖析工具,其中最常用的就是開發者工具中的 timeline 和 network,善用它們可以準確定位有關響應速度的風險區域。

https://cdn.auth0.com/blog/fasterweb/timeline.png

timeline 皮膚便於快速查詢耗時操作。

https://cdn.auth0.com/blog/fasterweb/network.png

network 皮膚便於定位由請求時間和序列載入引起的響應速度問題。

此外,如果合理分析記憶體的使用率,也將有效提高應用程式的效能。如果你的頁面中有大量的視覺元素(比如動態的表格)或者大量的互動元素(比如遊戲),那麼對記憶體使用的剖析就可以有效減少卡頓,提高幀速。如果你想了解如何在 Chrome 開發者工具中進行記憶體剖析,請參考這篇文章:《4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them 》

Chrome 開發者工具也可以對 CPU 的使用進行剖析,更多詳細資訊請參考來自谷歌文件的這篇文章:《 Profiling JavaScript Performance》

https://cdn.auth0.com/blog/fasterweb/cpu.png

找出效能的核心痛點,才能讓你更加高效地進行效能優化。

相對而言,對後端進行效能剖析稍顯困難。一般而言,從最耗時的請求入手查詢相應的伺服器是個不錯的方法。這裡並沒有推薦任何有關後端的效能剖析工具,這是因為具體的剖析工具要視具體的後端技術棧而定。

演算法

在大多數情況下,選擇更高效的演算法可以比區域性優化獲得更佳的收益。從某種意義上說,對 CPU 和記憶體進行效能剖析有助於幫助開發者找出應用程式中較大的效能瓶頸。如果這些瓶頸並不是由程式碼的錯誤引起的,那很有可能就是演算法的問題。

7. 負載均衡

在上文的快取一節中,簡單提到了內容分發網路(CDN)的概念。根據伺服器或者地理區域分發負載可以有效提高資源的響應速度,這一優勢在處理併發連結時尤為明顯。

簡而言之,負載均衡類似於一種輪詢方案,基於反向代理伺服器 nginx 或者成熟的分發網路(比如 CloudflareAmazon CloudFront 構建。

https://cdn.auth0.com/blog/fasterweb/diagram.png

為了實現負載均衡,需要將動態內容和靜態內容進行分離,便於執行並行連線。換言之,序列訪問削弱了負載均衡檢索最佳路徑並進行分發的能力。此外,並行載入資源還可以加快應用程式的啟動速度。

負載均衡也可以構建的很精細。如果資料模型不能夠很好地與最終的一致性演算法或快取保持良好的匹配關係,那麼必將導致諸多問題。幸運的是,大多數的應用程式所請求的資料都是一個縮減集,該縮減集本身具有較高階別的一致性。如果你的應用程式還沒有具備這樣的能力,那麼你需要考慮重構它了。

8. 同構 JavaScript

對於 web 應用程式來說,一個增強使用者體驗的法門就是減少啟動時間或者減少首屏渲染時間,這一點對於需要在客戶端執行大量邏輯操作的單頁應用尤為重要。在客戶端執行的邏輯操作越多,通常意味著需要在首屏渲染前載入更多的資源。同構 JavaScript 就是用來解決這一問題的:JavaScript 可以同時在客戶端和服務端執行,所以可以在服務端渲染出來首屏,然後將其傳送給客戶端,再由客戶端的 JavaScript 接手剩下的邏輯處理。這一方案限制了服務端只能基於 JavaScript 框架,但可以提高使用者體驗。目前,在 Meteor.js 中已經可以直接使用這一方式了。此外,在 React 框架中也可以採用這種方式,程式碼如下所示:

var React = require('react/addons');
var ReactApp = React.createFactory(require('../components/ReactApp').ReactApp);

module.exports = function(app) {

    app.get('/', function(req, res){
        // React.renderToString takes your component
        // and generates the markup
        var reactHtml = React.renderToString(ReactApp({}));
        // Output html rendered by react
        // console.log(myAppHtml);
        res.render('index.ejs', {reactOutput: reactHtml});
    });

};

下面是 Meteor.js 的簡單示例:

if (Meteor.isClient) {
  Template.hello.greeting = function () {
    return "Welcome to myapp.";
  };

  Template.hello.events({
    'click input': function () {
      // template data, if any, is available in 'this'
      if (typeof console !== 'undefined')
        console.log("You pressed the button");
    }
  });
}

if (Meteor.isServer) {
  Meteor.startup(function () {
    // code to run on server at startup
  });
}

如果你開發的是大中型複雜應用且支援同構釋出,那麼可以嘗試一下這種方式,效果很可能令人震撼。

9. 索引

如果資料庫查詢佔據了太多的執行時間,那麼你應該考慮優化資料庫的執行速度了。每種資料庫和資料模型都各有特色。資料庫優化有多種方向:資料模型、資料庫型別以及其他配置,所以優化起來並不簡單。不過,我們還是有一些通用的優化技巧,比如說:索引。索引根據資料庫的資料建立快速訪問的資料結構,改善對特定資料的檢索速度。現在大多數的資料庫都支援索引功能,

在使用索引優化資料庫之前,你應該研究當前應用程式的訪問模式,分析最常用到的查詢是什麼,哪一個鍵或者欄位會被頻繁查詢等等。

10. 編譯工具

JavaScript 技術棧日益複雜,這也推動了語言本身的進步。不幸的是,JavaScript 的發展目前還要受限於使用者的訪問環境。雖然 ECMAScript 2015 已經對 JavaScript 做出了諸多改進,但是開發者尚不能直接遵循這一規範的程式碼。針對這一問題,也就衍生出了諸多編譯工具,這些工具常用於將 ECMAScript 2015 的程式碼轉換為 ECMAScript 5 的程式碼。此外,模組打包和檔案壓縮也加入到了編譯過程,最終用於生成線上版本的程式碼。這些工具將程式碼轉換為了一個受限的版本,間接影響到了最終程式碼的執行效率。谷歌開發者 Paul Irish 測試了程式碼轉換對效能和檔案大小的影響,詳情請點選連結。雖然大多數情況下影響甚微,但這些差異仍然值得引起注意,因為隨著應用程式的複雜大增高,這些差異也將日益增大。

11. 阻塞渲染

JavaScript 和 CSS 資源的載入都會阻塞頁面的渲染過程。通過某些技巧,開發者可以儘快載入 JavaScript 和 CSS 資源,從而讓瀏覽器儘快顯示網站的內容。

對 CSS 來說,本質上符合當前頁面媒體屬性的 CSS 規則會具有較高的處理優先順序。頁面的媒體屬性由 CSS 的媒體查詢進行匹配。媒體查詢通知瀏覽器哪一個 CSS 指令碼針對哪一種媒體屬性。舉例來說,相對於當前螢幕顯示的 CSS,用於列印的 CSS 的優先順序較低。

可以為 <link> 標籤設定與媒體查詢有關的屬性:

<link rel="stylesheet" type="text/css" media="only screen and (max-device-width: 480px)" href="mobile-device.css" />

對 JavaScript 來說,關鍵是恰當地使用內嵌 JavaScript(即在 HTML 中的 JavaScript)。內嵌 JavaScript 應該儘可能簡短,且不能阻塞對頁面其他部分的阻塞。換言之,位於 HTML 文件樹之中的內嵌 JavaScript 會阻塞 HTML 指令碼的解析,強制解析引擎直到指令碼執行完成才能繼續解析。如果 HTML 樹中有大量這種阻塞指令碼或者阻塞時間過長,勢必嚴重破壞應用程式的使用者體驗。內嵌 JavaScript 有助於防止網路獲取過多的指令碼。對於反覆用到的指令碼,或者體積較大的指令碼,不建議使用內聯形式。

一種有效防止 JavaScript 阻塞 HTML 解析的方法是以非同步的方式載入 <script> 標籤。這種方式限制了我們隊 DOM 的訪問(無法使用 document.write),但可以讓瀏覽器在解析和渲染頁面的時候無需考慮 JavaScript 的執行狀態。換言之,為了獲取最佳的啟動速度,應該確保所有非必需的指令碼都要以非同步的形式載入:

<script src="async.js" async></script>

12. servce workers 和 stream

Jake Archibald 的最新文章 對提高渲染速度提出了一個很有意思的方案:結合 service workers 和 stream 進行頁面渲染。結果相當令人信服:

<iframe width="560" height="315" src="https://www.youtube.com/embed/Cjo9iq8k-bc" frameborder="0" allowfullscreen></iframe>

不幸的是,這一技巧所用到的 API 尚在變化之中,所以還不能應用於實際開發中。這一技巧的核心是在網站和客戶端之間存放一個 service worker。service worker 可以用於快取資料(比如網站的頭部等不常變動的部分),避免網路查詢失敗。如果快取資料丟失,可以通過 stream 快速獲取。

擴充套件閱讀

更多有關效能優化的資訊和工具請參考以下連結:

Best Practices for Speeding up Your Website - Yahoo Developer Network
YSlow - a tool that checks for Yahoo's recommended optimizations
PageSpeed Insights - Google Developers
PageSpeed Tools - Google Developers
HTTP/2: The Long-Awaited Sequel

結論

隨著應用程式變得越來越龐大和複雜,效能優化在 web 開發中的地位也越來越重要。針對性的效能優化至關重要,有助於降低時間成本和維護成本。web 應用程式歷經發展,其作用已經不再是單一的內容展現,學習通用的效能優化模式,可以將一個難以使用的應用程式轉為一個易於上手的工具。沒有任何規則是絕對的,只有不斷研究和剖析技術棧的深層次邏輯,才能合理進行效能優化。

相關文章