12步建立高性能Web APP

如今,Web App 日益重視用戶的交互體驗,瞭解性能優化的方式則能夠有效提升用戶體驗。閱讀和實踐下面的性能優化技巧,能夠幫你改善應用的流暢度、渲染時間和其餘方面的性能表現。javascript

概述

對 Web App 進行性能優化是一份冗雜沉重的工做,這不只是由於構建一個 Web App 須要先後端協做,並且須要多方面的技術棧:數據庫、後端、前端,須要運行在多種平臺:iOS,安卓,Chrome,Firefox,Edge。這太複雜了!不過,仍是有一些歷經實踐的通用方式能夠用來優化 Web App 的性能。在接下來的小節中,咱們將逐步介紹相關的細節。css

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

過早優化

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

開發者浪費了大量的時間去思考或者擔憂程序的執行速度,但實際上從調試和後期維護的角度看,這些優化措施每每會帶來嚴重的負面影響。咱們應該着重 97% 的運行表現:過早的性能優化是一切罪惡的根源。固然,咱們也不該該放棄 3% 的痛點。 ———— Donald Knuthjava

文件壓縮和模塊打包

JavaScript 一般是直接使用源碼的方式分發的,而源碼解析起來每每要慢於字節碼。對於小腳原本說,二者解析的速度並不大,但對於大的應用程序來講,則會明顯影響應用程序的啓動速度。解決這一痛點,正是 WebAssembly 的出發點之一,它將大幅改善程序的啓動速度。文件壓縮是剔除文件中無用字符的流程,雖然處理後的代碼喪失了可讀性,但提升了瀏覽器的解析速度。react

另外一方面,模塊打包能夠將不一樣的腳本合併爲一個腳本,從而下降 HTTP 請求,減小資源加載時間。一般來講,這種工做都會交給相應的工具來處理,好比 Webpackwebpack

function insert(i) { document.write("Sample " + i); } for(var i = 0; i < 30; ++i) { insert(i); } 

壓縮以後:nginx

!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 的信息請參考官方文檔git

按需加載

按需加載資源或者說懶加載資源(特別是圖片)對優化 Web App 的性能有很大幫助。對於圖片較多的頁面,使用懶加載一般有如下三點好處:angularjs

  • 減小併發請求,緩解服務器壓力,提升加載速度
  • 減小瀏覽器的內存佔用率
  • 下降服務器的負載

圖片或其餘資源懶加載的方案通常是,在程序啓動時加載首屏資源,在頁面滾動時持續加載即將進入視口的資源。因爲這種方法每每須要與頁面結構和開發方式相協調,因此經常使用現有的插件和擴展來實現惰性加載。舉例來講,react-lazy-load 是一個基於 React 的圖片惰性加載插件:

const MyComponent = () => ( <div> Scroll to load images. <div className="filler" /> <LazyLoad height={762} offsetVertical={300}> <img src='http://apod.nasa.gov/apod/image/1502/HDR_MVMQ20Feb2015ouellet1024.jpg' /> </LazyLoad> (...) 

一個典型的按需加載實例就是谷歌的圖片搜索工具,點擊這一連接並滾動頁面,打開開發者工具注意資源的加載時間。

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!)

array-ids

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

緩存

緩存經常使用來存儲頻繁調用的數據,當緩存後的數據再次被調用時,就能夠由緩存直接提供數據,提升數據的響應速度。一般來講,一個 Web App 都是由多個組件構成的,在這些組件中都能發現緩存的影子。好比動態內容服務器和客戶端之間使用的緩存,經過減小通用請求下降服務器負載,能夠改善頁面的響應時間;好比代碼中的緩存處理,能夠優化某些通用的腳本訪問模式。此外,還有數據庫緩存和長進程緩存等。

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

使用緩存能夠優化資源加載,好比,使用 basket.js 利用本地存儲緩存應用的腳本,在第二次調用資源時能夠迅速從本地存儲中得到相應的資源。

Amazon CloudFront 是如今比較流行的一項緩存服務。CloudFront 的工做機制相似內容分發網絡(CDN),能夠爲動態內容設置緩存。

HTTP/2

目前,已經有愈來愈多的瀏覽器支持 HTTP/2。HTTP/2 的優點在於它與服務器的併發鏈接,好比,若是須要加載的小型資源(前提是你不對資源進行打包)比較多,HTTP/2 在響應時間和性能上都要遠遠優勝於 HTTP/1。你能夠點擊 Akamai 的 HTTP/2 示例 查看二者的區別。

HTTP/2

性能剖析

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

對 Web 應用程序來講,響應速度是一個很是重要的衡量指標,因此開發者都會盡量地去提升資源的加載速度和頁面的渲染速度。Chrome 瀏覽器提供了一系列優秀的性能剖析工具,其中最經常使用的就是開發者工具中的 timeline 和 network,善用它們能夠準肯定位有關響應速度的風險區域。

性能剖析

timeline 面板便於快速查找耗時操做。

性能剖析

network 面板便於定位由請求時間和串行加載引發的響應速度問題。

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

Chrome 開發者工具也能夠對 CPU 的使用進行剖析,更多詳細信息請參考來自谷歌文檔的這篇文章:《 Profiling JavaScript Performance》

CPU剖析

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

相對而言,對後端進行性能剖析稍顯困難。通常而言,從最耗時的請求入手查找相應的服務器是個不錯的方法。這裏並無推薦任何有關後端的性能剖析工具,這是由於具體的剖析工具要視具體的後端技術棧而定。

算法

在大多數狀況下,選擇更高效的算法能夠比局部優化得到更佳的收益。從某種意義上說,對 CPU 和內存進行性能剖析有助於幫助開發者找出應用程序中較大的性能瓶頸。若是這些瓶頸並非由代碼的錯誤引發的,那頗有可能就是算法的問題。

負載均衡

在上文的緩存一節中,簡單提到了內容分發網絡(CDN)的概念。根據服務器或者地理區域分發負載能夠有效提升資源的響應速度,這一優點在處理併發連接時尤其明顯。

簡而言之,負載均衡相似於一種輪詢方案,基於反向代理服務器 nginx 或者成熟的分發網絡(好比 Cloudflare 和 Amazon CloudFront 構建。

負載均衡

爲了實現負載均衡,須要將動態內容和靜態內容進行分離,便於執行並行鏈接。換言之,串行訪問削弱了負載均衡檢索最佳路徑並進行分發的能力。此外,並行加載資源還能夠加快應用程序的啓動速度。

負載均衡也能夠構建的很精細。若是數據模型不可以很好地與最終的一致性算法或緩存保持良好的匹配關係,那麼必將致使諸多問題。幸運的是,大多數的應用程序所請求的數據都是一個縮減集,該縮減集自己具備較高級別的一致性。若是你的應用程序尚未具有這樣的能力,那麼你須要考慮重構它了。

同構 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 }); } 

若是你開發的是大中型複雜應用且支持同構發佈,那麼能夠嘗試一下這種方式,效果極可能使人震撼。

索引

若是數據庫查詢佔據了太多的執行時間,那麼你應該考慮優化數據庫的執行速度了。每種數據庫和數據模型都各有特點。數據庫優化有多種方向:數據模型、數據庫類型以及其餘配置,因此優化起來並不簡單。不過,咱們仍是有一些通用的優化技巧,好比說:索引。索引根據數據庫的數據建立快速訪問的數據結構,改善對特定數據的檢索速度。如今大多數的數據庫都支持索引功能,

在使用索引優化數據庫以前,你應該研究當前應用程序的訪問模式,分析最經常使用到的查詢是什麼,哪個鍵或者字段會被頻繁查詢等等。

編譯工具

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

阻塞渲染

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> 

servce workers 和 stream

Jake Archibald 的最新文章 對提升渲染速度提出了一個頗有意思的方案:結合 service workers 和 stream 進行頁面渲染。結果至關使人信服:

不幸的是,這一技巧所用到的 API 尚在變化之中,因此還不能應用於實際開發中。這一技巧的核心是在網站和客戶端之間存放一個 service worker。service worker 能夠用於緩存數據(好比網站的頭部等不常變更的部分),避免網絡查找失敗。若是緩存數據丟失,能夠經過 stream 快速獲取。

擴展閱讀

更多有關性能優化的信息和工具請參考如下連接:

結論

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

相關文章
相關標籤/搜索