[譯] 惟快不破:Web 應用的 13 個優化步驟

歡迎關注知乎專欄 —— 前端的逆襲
歡迎關注個人博客知乎GitHubjavascript


譯文地址:【譯】惟快不破:Web 應用的 13 個優化步驟 - 前端的逆襲 - 知乎專欄
原文地址:12 Steps to a Faster Web App -- Auth0css

時過境遷,Web 應用比以往任什麼時候候都更具交互性。搞定性能能夠幫助你極大地改善終端用戶的體驗。閱讀如下的技巧並學以至用,看看哪些能夠用來改善延遲,渲染時間以及總體性能吧!html

更快的 Web 應用

優化 Web 應用是一項費勁的工做。Web 應用不只處於客戶端和服務器端的兩部分組件當中,一般來講也是由多種多樣的技術棧構建而成:數據庫,後端組件(通常也是搭建在不一樣技術架構之上的),以及前端(HTML + JavaScript + CSS + 轉化器)。運行時也是變化無窮的:iOS,Android,Chrome,Firefox,Edge。若是你曾經工做在一個不一樣的單一龐大的平臺之上,一般狀況下性能優化只針對於單一目標(甚至只是目標的單一版本而已),可是如今的話你就可能會意識到任務複雜度要遠超於此。這就對了。但這兒也有一些通用的優化指南能夠大大優化一個應用。咱們將會在接下來的章節中探討這些指南的內容。前端

一份 Bing 的研究代表,頁面加載時間每增長 10ms,網站的年收入就會減小 25 萬美圓。 —— Rob Trace 和 David Walp,微軟高級程序經理java

過早優化?

優化最難的地方就是如何在開發生命週期中最適當的時候去作優化。Donald Knuth 有一句名言:_「過早優化乃萬惡之源」_。這句話背後的緣由很是簡單:由於一不當心就會浪費時間去優化某個 1% 的地方,可是結果卻並不會對性能形成什麼重大影響。與此同時,一些優化還妨礙了可讀性或者是可維護性,甚至還會引入新的 Bug。換句話說,優化不該當被認爲是「意味着獲得應用程序的最佳性能」,而是「探索優化應用的_正確的方式_,並獲得_最大的效益_」。再換句話說,盲目的優化可能會致使效率的丟失,而收益卻很小。在你應用如下技巧的時候請將此銘記在心。你最好的朋友就是分析工具:找到你能夠進行經過優化得到最大程度改善的性能點,而不用損害應用開發的進程或者可維護性。react

程序員們浪費了大量時間來思考,或者說是擔心,他們的程序中非關鍵部分的運行速度。而且他們對於性能的這些嘗試,實際上卻對代碼的調試和維護有着很是消極的影響。咱們應當忘記那些不重要的性能影響,在 97% 的時間裏均可以這麼說:過早優化乃萬惡之源。固然咱們也不該當在那關鍵的 3% 上放棄咱們的機會。—— Donald Knuthlinux

1. JavaScript 壓縮和模塊打包

JavaScript 應用是以源碼形式進行分發的,而源碼解析的效率是要比字節碼低的。對於一小段腳原本說,區別能夠忽略不計。可是對於更大型的應用,腳本的大小會對應用啓動時間有着負面的影響。事實上,寄指望於使用 WebAssembly 而得到最大程度的改善,其中之一就是能夠獲得更快的啓動時間。webpack

另外一方面,模塊打包則用於將不一樣腳本打包在一塊兒並放進同一文件。更少的 HTTP 請求和單個文件解析均可以減小加載時間。一般狀況下,單獨一種工具就能夠處理打包和壓縮。Webpack 就是其中之一。ios

示例代碼:nginx

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 應用在總體上得到更好的性能。對於使用大量圖片的頁面來講惰性加載有着顯著的三個好處:

  • 減小向服務器發出的併發請求數量(這就使得頁面的其餘部分得到更快的加載時間)

  • 減小瀏覽器的內存使用率(更少的圖片,更少的內存)

  • 減小服務器端的負載

大致上的理念就是隻在必要的時候纔去加載圖片或資源(如視頻),好比在第一次被顯示的時候,或者是在將要顯示的時候對其進行加載。因爲這種方式跟你建站的方式密切相關,惰性加載的解決方案一般須要藉助其餘庫的插件或者擴展來實現。舉個例子,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>
    (...)

一個很是好的實踐範例就像 Goggle Images 的搜索工具同樣。點擊前面的連接而且滑動頁面滾動條就能夠看到效果了。

3. 在使用 DOM 操做庫時用上 array-ids

若是你正在使用 ReactEmberAngular 或者其餘 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 使得 DOM 操做引擎能夠「知道」在何時某個節點能夠被映射到數組當中的某個元素。沒有 array-ids 或者 track-by 的話,大部分庫都會進行從新排序而摧毀已有的節點並從新建立新的。這就很是損耗性能了。

4. 緩存

Caches 是用於存儲那些被頻繁存取的靜態數據的組件,便於隨後對於這個數據的請求能夠更快地被響應,或者說請求方式更加高效。因爲 Web 應用是由不少可拆卸的部件組合而成,緩存就能夠存在於架構中的不少部分。舉例來講,緩存能夠被放在動態內容服務器和客戶端之間,就能夠避免公共請求以減小服務器的負載,與此同時改善響應時間。其餘緩存可能被放置在代碼裏,以優化某些用於腳本存取的通用模式,還有些緩存可能被放置在數據庫或者是長運行進程以前。

簡而言之,在 Web 應用中使用緩存是一種改善響應時間和減小 CPU 使用的絕佳方式。難點就在於搞清楚哪裏纔是在架構中存放緩存的地方。再一次,答案就是性能分析:常見的瓶頸在哪裏?數據或者結果可緩存嗎?他們都太容易失效嗎?這都是一些棘手的問題,須要從原理上來一點一點回答。

緩存的使用在 Web 環境中富有創造性。好比,basket.js 就是一個使用_Local Storage_ 來緩存應用腳本的庫。因此你的 Web 應用在第二次運行腳本的時候就能夠幾乎瞬間加載了。

現在一個廣受歡迎的緩存服務就是亞馬遜的 CloudFront。CloudFront 就跟一般的內容分發網絡(CDN)用途同樣,能夠被設置做爲動態內容的緩存。

5. 啓用 HTTP/2

愈來愈多的瀏覽器都開始支持 HTTP/2。這可能聽起來沒有必要,可是 HTTP/2 爲同一服務器的併發鏈接問題帶來了不少好處。換句話說,若是有不少小型資源須要加載(若是你打包過的話就沒有必要了),在延遲和性能方面 HTTP/2 秒殺 HTTP/1。試試 Akamai 的 HTTP/2 demo,能夠在最新的瀏覽器中看到區別。

6. 應用性能分析

性能分析是優化任何應用程序時的重要一步。就像介紹中所提到的那樣,盲目嘗試優化應用常常會致使效率的浪費,微不足道的收益和更差的可維護性。執行性能分析是識別你的應用問題所在的一個重要步驟。

對於 Web 應用來講,延遲時間是最大的抱怨之一,因此你須要確保數據的加載和顯示都儘量得快。Chrome 提供了很是棒的性能分析工具。特別是 Chrome Dev Tools 中的時間線和網絡視圖都對於定位延遲問題有着很大的幫助:

時間線視圖能夠幫忙找到運行時間較長的操做。

網絡視圖能夠幫助識別出額外的由緩慢請求致使的延遲或對於某一端點的串行訪問。

正確分析的話,內存則是另外一塊可能得到收益的部分。若是你正在運行着一個擁有不少虛擬元素的頁面(龐大的動態表格)或者可交互式的元素(好比遊戲),內存優化能夠得到更少的卡頓和更高的幀率。從咱們最近的文章 4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them 中,對於如何使用 Chrome 的開發工具備着進一步的深度理解。

CPU 性能分析也能夠在 Chrome Dev Tools 中找到。看看這篇來自 Google 官方文檔中的文章 Profiling JavaScript Performance

找到性能損耗的中心可讓你有效率地達到優化的目標。

對後端的性能分析會更加困難。一般狀況下,確認一個耗費較多時間的請求可讓你明確應該優先分析哪個服務。對於後端的分析工具來講,則取決於所構建的技術棧。

一個關於算法的注意事項

在大多數狀況下,選擇一個更優的算法,比圍繞着小成本中心所實現的具體優化策略可以得到更大的收益。在某種程度上,CPU 和內存分析應該能夠幫你找到大的性能瓶頸。當這些瓶頸跟編碼問題並不相關時,則是時候考慮考慮不一樣的算法了。

7. 使用負載均衡方案

咱們在以前討論緩存的時候簡要提到了內容分發網絡(CDNs)。把負載分配到不一樣的服務器(甚至於不一樣的地理區域)能夠給你的用戶提供更好的延遲時間,可是這條路還很漫長,特別是在處理不少的併發鏈接的時候。

負載均衡就跟使用某個 round-robin(循環)解決方案同樣簡單,能夠基於一個 nginx 反向代理 ,或者基於一個成熟的分佈式網絡,好比 Cloudflare 或者 Amazon CloudFront

以上的圖來自於 Citrix。 爲了使負載均衡真正有效,動態內容和靜態內容都應該被拆分紅易於併發訪問的。換句話說,元素的串形訪問會削弱負載均衡器以最佳形式進行分流的能力。與此同時,對於資源的併發訪問能夠改善啓動時間。

雖然負載均衡可能會很複雜。對最終一致性算法不友好的數據模型,或者緩存都會讓事情更加困難。幸運的是,大多數應用對於已簡化的數據集都只須要保證高層次的一致性便可。若是你的應用程序沒有這樣設計的話,就有必要重構一下了。

8. 爲了更快的啓動時間考慮一下同構 JavaScript

改善 Web 應用程序觀感的方式之一,就是減小啓動時間或者減小首頁渲染時間。這對於新興的單頁面應用尤其重要,其須要在客戶端執行大量任務。在客戶端作更多事情一般就意味着,在第一次渲染被執行以前就須要下載更多的信息。同構 JavaScript 能夠解決這個問題:自從 JavaScript 能夠同時運行在客戶端和服務器端,這就讓在服務器端來執行頁面的首次渲染成爲可能,先把已渲染的頁面發送出去而後再由客戶端的腳本接管。這限制了所使用的後端(必須使用支持該特性的 JavaScript 框架),但卻能得到更好的用戶體驗。舉例來講,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 對於客戶端和服務器端的 JavaScript 混用有着很是棒的支持。

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

可是,爲了支持服務器端渲染,須要像 meteor-ssr 這樣的插件。

謝謝 gabrielpoca 在評論中指出這一點。若是你有複雜的或者中等大小的應用須要支持同構部署,試試這個,你可能會感到驚訝的。

9. 使用索引加速數據庫查詢

若是你須要解決數據庫查詢耗費大量時間的問題(分析你的應用看看是不是這種狀況!),是時候找出加速數據庫的方法了。每一個數據庫和數據模型都有本身的權衡。數據庫優化在每一方面都是一個主題:數據模型,數據庫類型,具體實現方案,等等。提速可能不是那麼的簡單。可是這兒有個建議,可能能夠對某些數據庫有所幫助:索引。索引是一個過程,即數據庫所建立的快速訪問數據結構,從內部映射到鍵(在關係數據庫中的列),能夠提升檢索相關數據的速度。大多數現代數據庫都支持索引。索引並非文檔型數據庫(好比 MongoDB)所獨有的,也包括關係型數據庫(好比PostgreSQL)。

爲了使用索引來優化你的查詢,你將須要研究一下應用程序的訪問模式:什麼是最多見的查詢,在哪一個鍵或列中執行搜索,等等。

10. 使用更快的轉譯方案

JavaScript 軟件技術棧一如既往的複雜。而改善語言自己的需求則又增長了複雜度。不幸地是,JavaScript 做爲目標平臺又會被用戶的運行時所限制。儘管不少改進已經以 ECMAScript 2015(2016正在進行)的形式實現了,可是一般狀況下,對客戶端代碼來講又不可能依賴於這個版本。這種趨勢促使了一系列的_轉譯器_:用於處理 ECMAScript 2015 代碼的工具和只使用 ECMAScript 5 結構實現其中所缺失的特性。與此同時,模塊綁定和壓縮處理也已經被集成到這個生產過程當中,被稱爲_爲發佈而構建_的代碼版本。這些工具能夠轉化代碼,而且可以以有限的方式影響到最終代碼的性能。Google 開發者 Paul Irish 花了一些時間來尋找這些轉譯方案會如何影響性能和最終代碼的大小。儘管大多數狀況下收益會很小,但也值得在正式採用某個工具棧以前看看這些數據。對於大型應用程序來講,這種區別可能會影響重大。

11. 避免或最小化 JavaScript 和 CSS 的使用而阻塞渲染

JavaScript 和 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 應該儘量短,並將其放在不會阻塞頁面剩餘部分解析的地方。換句話說,被放在 HTML 樹中間的內聯 JavaScript 將會在這個地方阻塞解析器,並強制其等待直到腳本被執行完畢。若是在 HTML 文件中隨意放了一些大的代碼塊或者不少小的代碼塊,對於性能來講這會成爲性能殺手。內聯能夠有效減小額外對於某些特定腳本的網絡請求。可是對於重複使用的腳本或者大的代碼塊來講,這個好處就能夠忽略不計了。

防止 JavaScript 阻塞解析器和渲染器的一種方法就是將 <script> 標籤標記爲_異步的_。這限制了咱們對於 DOM 的訪問可是可讓瀏覽器無論腳本的執行狀態而繼續解析和渲染頁面。換句話說,爲了得到最佳的啓動時間,確保那些對於渲染不重要的腳本已經經過異步屬性的方式標記成異步的了。

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

12. 用於將來的一個建議:使用 service workers + 流

Jake Archibald 最近的一篇博文詳細描述了一種有趣的技術,能夠用於加速渲染時間:將 service workers 和流結合起來。結果很是使人歎服:

不幸的是這個技術所須要的 APIs 都還不穩定,這也是爲何這是一種有趣的概念但如今尚未真正被應用的緣由。這個想法的主旨就是在網站和客戶端之間放置一個 service worker。這個 service worker 能夠在獲取缺失信息的同時緩存某些數據(好比 header 和一些不會常常改變的東西)。缺失的內容就能夠儘量快速地流向被渲染的頁面。

https://www.youtube.com/watch?v=Cjo9iq8k-bc

13. 更新:圖片編碼優化

咱們的一個讀者指出了一個很是重要的遺漏:圖片編碼優化。PNGs 和 JPGs 在 Web 發佈時都會使用次優的設置進行編碼。經過改變編碼器和它的設置,對於須要大量圖片的網站來講能夠得到有效的改善。流行的解決方案包括 OptiPNGjpegtran

A guide to PNG optimization 詳細描述了 OptiPNG 能夠如何用於優化 PNGs。

The man page for jpegtran 對它的一些特性提供了很好的介紹。

若是你發現這些指南相對於你的要求來講都太複雜了的話,這兒有一些在線網站能夠提供優化服務。也有一些像 RIOT 同樣的圖形化界面,很是有助於批量操做和結果檢查。

擴展閱讀

你能夠在下面的連接中閱讀更多信息,以及找到有助於優化網站的工具:

悄悄話:Auth0 中常見的優化

咱們是一個 Web 公司。就以這種身份來講,咱們爲咱們的基礎設施的某些部分部署了一些特定的優化。舉例來講,在登陸頁面你能夠發現,在咱們域名的 /learn 路徑下(好比,登陸頁面的單點登陸),咱們採用了一種特別的優化:爲了方便咱們使用 CMS 來建立每篇文章。由於文章都沒有中心索引,可是爲了可以被搜索引擎發現,使用了 webtask 的爬蟲來預渲染每一個頁面並生成了一個靜態版本而後上傳到咱們 CDN。這減小了咱們在服務器端上的壓力,由於無須爲每一個訪客都生成動態的服務器端內容。與此同時還改善了延遲(而且隔離了咱們發現與 CMS 相關的安全問題)。

對於文檔部分,咱們正在使用_同構 JavaScript_,這讓咱們得到了很是棒的啓動時間,而且使咱們的後端和前端團隊可以輕鬆集成。

結論

因爲應用程序變得愈來愈大和愈來愈複雜,性能優化對於 Web 開發來講正在變得愈來愈重要。在作出任何值得的時間和潛在的將來成本的優化嘗試時,有針對性的改進都是必不可少的。Web 應用程序早已突破了大多數靜態內容的邊界,學習常見模式進行優化則是使人愉悅的應用和徹底不可用的應用之間最大的區別(這是讓你的訪客留下來的長遠之計!)。沒有什麼規則是絕對的,可是:性能分析和研究特定軟件技術棧的錯綜複雜之處,是找出如何優化它的惟一方式。你曾經發現過對你的應用產生巨大影響的其餘建議嗎?請留言讓咱們知道。Hack on!


歡迎關注知乎專欄 —— 前端的逆襲
歡迎關注個人博客知乎GitHub

相關文章
相關標籤/搜索