最近更新了咱們的網站,它是通過了設計上的全面驗收的。但實際上,做爲軟件開發者,咱們會注重不少技術相關的零碎的東西。咱們的目標是控制性能,注重性能,將來可伸展,爲網站增添內容是一種樂趣。接着就來告訴你,爲何咱們的網站速度比大家的快吧(抱歉,確實是這樣的)。css
在咱們的項目中,咱們天天都會和設計師和產品負責人討論關於平衡美觀和性能的問題。對於咱們本身的網站,這樣作是很簡單的。簡言之,咱們認爲好的用戶體驗從快速的內容傳輸開始,也就意味着 性能 > 美觀
。html
好的內容、佈局、圖片和交互是吸引用戶的重要因素。這每一個因素都會影響頁面的加載時間和終端用戶體驗。每一步咱們都在探討如何在得到好的用戶體驗和保證設計美感的同時,最小化對性能的影響。前端
咱們想要把核心內容儘快地呈現給用戶,意味着咱們要處理好基本的 HTML 和 CSS。每一個頁面都應該達到基本的目的:傳遞信息。JS、CSS、網頁字體、圖片、網站分析等優化都是位居於核心內容之下的。git
給理想網站定義了標準後,咱們總結出:要想達到預期效果,就要能對網站各方面的控制都遊刃有餘。咱們選擇構建本身的靜態站點生成器,包括資源傳輸,而且由咱們本身操控。web
咱們用 Node.js 實現了靜態站點生成器。它是採用帶有簡短 JSON
頁面描述標籤的 Markdown
文件來生成整個網站結構和它全部的資源。爲了包括特殊的頁面腳本,也能夠附帶一個 HTML
文件。如下是一個簡單化的描述標籤和 markdown
文件,用於博客的發佈,用它來生成真正的 HTML
。apache
JSON
描述標籤:gulp
{
後端
"keywords": ["performance", "critical rendering path", "static site", "..."],
瀏覽器
"publishDate": "2016-07-13",
緩存
"authors": ["Declan"]
}
markdown 文件:
# Why our website is faster than yours
We've recently updated our site. Yes, it has a complete...
## Design for performance
In our projects we have daily discussions...
平均一個 2406kb
的網頁中 1535kb
是圖片。就由於圖片在網站中佔據了這麼大的一個比例,因此它也是性能優化的重點之一。
WebP是一種現代圖片格式,爲網頁圖片提供了出色的低損耗、有損壓縮。WebP格式的圖片實質上比其它格式的小,有時能夠比一樣的 JPEG 圖片小 25%。 WebP被大多數人所忽略,也沒被常用。截止到寫這篇文章的時候,WebP 僅支持Chrome, Opera 和 Android (仍超過了咱們50%的用戶),但咱們能夠優雅降級爲 JPG/PNG。
使用 <picture>
元素咱們能夠把圖片從 WebP 優雅地降級到其它被普遍支持的圖片格式,如JPEG:
<picture>
<source type="image/webp" srcset="image-l.webp" media="(min-width: 640px)">
<source type="image/webp" srcset="image-m.webp" media="(min-width: 320px)">
<source type="image/webp" srcset="image-s.webp">
<source srcset="image-l.jpg" media="(min-width: 640px)">
<source srcset="image-m.jpg" media="(min-width: 320px)">
<source srcset="image-s.jpg">
<img alt="Description of the image" src="image-l.jpg">
</picture>
咱們使用Scott Jehl 的 picturefill
來使那些不支持 <picture>
元素的瀏覽器得到支持,在各個瀏覽器中達到一致的效果
咱們使用 <img>
做爲那些不支持 <picture>
或者 JS 的瀏覽器的後備元素。使用圖片的最大實例確保了它在後備方案中的可行性。
儘管圖片傳輸方式已經肯定了,咱們仍須要思考該怎樣有效地執行。我喜歡 <picture>
元素的功能,但不喜歡寫上面那些代碼段,尤爲是寫內容時必須把它加進去。咱們不想作這麼費力的事情:每張圖片都要寫 6
個實例,因此優化這些圖片而且把它們寫在markdown文件的 <picture>
裏面。因此:
在構建過程當中,原圖片的多個實例,包括JPG, PNG和WebP格式,咱們使用 gulp responsive
來生成。
[圖片描述](image.jpg)
.在構建過程當中使用自定義Markdown渲染器來爲已經徹底成熟的 <picture>
元素編譯傳統的markdown圖片聲明。
咱們爲本身的網站選擇了特定的圖標類型,其中SVG插圖佔了主要地位。這樣作有如下幾個緣由:
首先,SVG的圖片比位圖更小;
其二,SVG圖片自己就是響應式的,有很棒的伸縮性, 因此不須要圖片生成及 <picture>
元素;
最後也是很重要的一點,就是咱們能夠經過CSS來不斷改變它,賦予它新的活力!咱們全部的組合頁面都有一個自定義的動態SVG圖, 能夠被概述頁面所複用。這張圖片做爲咱們全部組合頁面的一種循環風格,使得頁面設計一體化,同時又幾乎不會對性能形成影響。請看這張動畫,看看咱們是如何用CSS來改變它的。
在深刻以前,這裏有一個關於在瀏覽器設置自定義字體的簡短介紹。當瀏覽器發現CSS裏面有@font-face
的定義,可是用戶的電腦並不支持該字體時,它會嘗試下載該字體文件。在下載時,多數瀏覽器根本不會用這種字體來展現文本。這種現象稱爲「不可見文本的閃現」 或者 FOIT
。若是你有留意,你會發現網頁上都有這種狀況存在。若是你問我,我會告訴你這會影響用戶體驗。它延遲了用戶讀取他們所需內容的時間。咱們能夠迫使瀏覽器改變這種行爲,變成 「無樣式內容閃現」 或者稱爲 FOUT
。咱們告訴瀏覽器先使用普通字體,像 Arial 或者 Georgia。當自定義的字體下載完成後,再代替標準字體而且從新渲染。這樣,即便自定義字體下載失敗,仍然不會影響內容的可讀性。然而,有人會認爲這是一種妥協的作法,但咱們認爲自定義字體只是一種優化。儘管沒有自定義字體,網頁看起來也無缺,也能百分百的正常運行。勾選/不勾選複選框來切換咱們的網頁字體,來本身體驗一下:
切換下載的字體類
使用自定義網頁字體能夠改善咱們的用戶體驗,只要你可以優化他們,而且負責任地爲之服務。
到目前爲止,子集設定是改善網頁字體性能最快的方式。我將會向每一個使用自定義字體的網頁開發者推薦它。若是你能徹底控制網頁內容,而且知道它將要展現哪些特性的話,你能夠徹底使用子集設定。可是,即便是僅僅把字體設爲西方語言,也會對文件大小形成很大的影響。例如,咱們的 Noto Regular WOFF
字體,默認是246KB,將其設爲西方語言後,大小降低到31KB。咱們使用 Font squirrel webfont
, 這種字體真的很易用。
Bram Stein 推出的字體監聽器是一個很了不得的腳本,能夠幫助檢查字體是否已被加載。至於你是如何加載字體的,是經過一個網頁字體服務,仍是本身上傳就不可知了。在這個監聽器告訴咱們全部自定義的字體已經下載完畢後,咱們就能夠在 <html>
元素上添加一個字體加載完畢的類,咱們的頁面就會從新用新的字體:
html {
font-family: Georgia, serif;
}
html.fonts-loaded {
font-family: Noto, Georgia, serif;
}
注意: 爲了簡短,我沒有給上面CSS中的 Noto
加上 @font-face
的聲明。
咱們能夠設定一個cookie來記住全部的字體已經被加載過,就可讓他們緩存在瀏覽器裏面了。咱們使用這個cookie來作重複的瀏覽,這個我後續會解釋。
在不久的未來,咱們或許不須要 Bram Stein 的腳原本監聽這個行爲。CSS開發團隊已經提案一個新的 @font-face
描述器,也叫 font-display
。它的屬性值控制着一個可下載的字體是如何在還沒加載出來時就渲染頁面的。這是CSS對font-display
的描述:它將帶給咱們像上面方法同樣的行爲效果。你能夠讀讀更多關於 font-display
的屬性。
通常來說,咱們都是儘量快的加載須要的資源。咱們移除一些堵塞的請求來加快頁面渲染,優化首屏,用瀏覽器緩存來處理重複的頁面。
設計上,咱們的網站並無不少JS。咱們發展了一個JavaScript工做流來處理咱們目前已有的js, 以及將來會用到的js資源。
JS在 <head>
塊裏面渲染,這是咱們想要的。JS應該只是用來提升用戶體驗,不該該是訪問者須要的關鍵。處理JS堵塞渲染的簡單方法就是把腳本放在頁面的尾部。這樣網頁就會在整個HTML 渲染完畢後纔去加載JS。
另外一種能夠把腳本放在 head
執行的方案是在 <script>
標籤裏面添加 defer
屬性,來延遲腳本的執行。因爲瀏覽器下載腳本是很快的,不會堵塞頁面渲染進程,等到頁面徹底加載完,纔會執行腳本里面的代碼。還有一件事,咱們沒有使用像jQuery這些庫,因此咱們的腳本取決於 vanilla
腳本的特性。咱們只是想要在瀏覽器加載腳原本支持這些特性。最終的結果就像下面的代碼這樣:
<script>
if ('querySelector' in document && 'addEventListener' in window) {
document.write('<script src="index.js" defer><\/script>');
}
</script>
咱們把這小段腳本放在頁面頭部,來檢測瀏覽器是否支持原生JavaScript的 document.querySelector
和window.addEventListener
屬性。若是支持,咱們經過 <script>
標籤直接給頁面加載腳本,並使用 defer
屬性讓它不會堵塞頁面渲染。
對於首屏來說,網站最大的渲染堵塞資源就是CSS了。只有當 <head>
裏面的CSS文件徹底加載完畢時,瀏覽器纔會開始渲染頁面。這種作法是通過深思熟慮的,若不是這樣,瀏覽器就須要在整個渲染過程當中不斷從新計算佈局尺寸,不斷重繪。
爲了防止CSS堵塞渲染,咱們就須要異步加載CSS文件。咱們使用了 Filament Group
的 awesome loadCSS
函數。該函數提供了一個回調,當CSS文件加載完後,咱們設置一個cookie來聲明CSS文件已經加載了。咱們使用這個cookie來重現頁面,這一點我後續會解釋到。
CSS異步加載也帶來這樣一個問題,儘管 HTML 真的很快被渲染出來,但看起來就只有純粹的HTML,只有等到整個CSS文件加載完且中止時,纔會有樣式。這時就要提到關鍵CSS
了。
關鍵CSS就是阻塞瀏覽器渲染出用戶可識別的網頁的一小部分CSS。咱們注意網頁的 上半版版面
。很明顯,兩個設備的版面的位置有很大的區別。所以,咱們作了一個大膽的猜想。
手動地檢測這部分關鍵性的CSS是個很耗時的過程,尤爲是樣式、特性等不斷改變時。這裏有幾個好的腳本,能夠在你構建網頁時,生成關鍵性CSS。咱們採用了 Addy Osmani
的版本。
下面是咱們分別用關鍵性CSS和整份CSS分別渲染的頁面效果。注意到下半版仍然有部份內容尚未樣式。
左側網頁是用關鍵CSS渲染的,而右側網頁則是用整份的CSS。紅線是分界線。
咱們本身部署 de Voorhoede
網站,由於咱們但願可以控制服務器環境。咱們也想要嘗試,是否能夠經過改變服務端的配置來大大提高性能。當前咱們使用了 Apache
服務和 HTTPS 協議。
爲了提高性能和安全性,咱們研究瞭如何配置服務端。
咱們使用 H5BP
apache樣板配置,這個對於改善Apache網絡服務的性能和安全性是個很好的開始。他們也有其餘服務器環境的配置。對於咱們的大部分 HTML、CSS 和 JS,咱們使用GZIP壓縮。對於咱們的全部網站資源,都使用緩存HTTP標頭的作法。有興趣請閱讀文件層級緩存的章節。
用HTTPS來服務你的網站會對性能形成影響。這主要是設置了SSL握手,引入了大量潛在的東西。但一般狀況下,咱們能夠作一些改變!
HTTP Strict Transport Security
是一個HTTP標頭,可讓服務器告訴瀏覽器只能用HTTPS來與它交互。這種方式防止HTTP請求被重定向爲HTTPS。全部嘗試用HTTP來訪問站點的請求都會被自動轉換成HTTPS。這樣就節省了一個來回。
TLS false start
容許客戶端在第一個TLS回合結束後,立刻向後端發送加密的數據。這種優化爲一個新的TLS鏈接減小了握手次數。一旦客戶端知道了密鑰,就能夠開始傳輸應用數據。剩下的握手用來確認是否有人篡改了握手記錄,而且能夠並行處理。
TLS session resumption
經過確認瀏覽器和服務器是否已經取得聯繫來幫咱們節省一個來回。瀏覽器會記住這一次的標識符,下次發起鏈接時,這個標識符就會被重用,節省了一個來回。
我聽起來像是一個搞開發和運維的,但確實不是。我只是讀過一些書,看過一些視頻。我喜歡 Google I/O 2016 的Mythbusting HTTPS
:Emily Stark
的安全性都市傳奇。
咱們沒有用一門服務端的語言,只有靜態的 Apache 網絡服務。但一個 Apache 網絡服務仍能夠作包括SSI在內的後端服務,而且讀取cookies。經過巧用cookies和運行那部分被Apache改寫的HTML,咱們能夠大大提高前端性能。下面這個例子就是了(咱們實際的代碼比這個複雜點,可是思想上是一致的):
<!-- #if expr="($HTTP_COOKIE!=/css-loaded/) || ($HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css' )"-->
<noscript><link rel="stylesheet" href="0d82f.css"></noscript>
<script>
(function() {
function loadCSS(url) {...}
function onloadCSS(stylesheet, callback) {...}
function setCookie(name, value, expInDays) {...}
var stylesheet = loadCSS('0d82f.css');
onloadCSS(stylesheet, function() {
setCookie('css-loaded', '0d82f', 100);
});
}());
</script>
<style>/* Critical CSS here */</style>
<!-- #else -->
<link rel="stylesheet" href="0d82f.css">
<!-- #endif -->
Apache 服務端邏輯看起來像一行一行的備註,通常以<!-- #
開始。咱們一步一步來看下吧:
$HTTP_COOKIE!=/css-loaded/
檢測是否存在CSS緩存cookie。 $HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css'
檢測緩存的版本是否是當前所要的版本。
If <!-- #if expr="..." -->
若是是true的話,咱們就假定這是用戶的第一次瀏覽。
第一次瀏覽咱們添加了 <noscript>
標籤,裏面放置了 <link rel="stylesheet">
。之因此這樣作,是由於咱們要異步加載整份CSS和JS。若是JS不能用的話,這種作法是不能執行的。這意味着,咱們要用常規的加載CSS的方法來作回退。
咱們添加了一個行內的腳原本懶加載CSS,onloadCSS
回調裏面能夠設置cookies.
在同一份腳本里面,咱們異步加載了整份CSS。
在 onloadCSS
的回調裏面,咱們用版本號來設置cookie的值。
在這個腳本後面,咱們添加了一行關鍵CSS的樣式。這個會阻塞渲染,但時間是很是短的,並且能夠避免頁面展現出來只有純粹的HTML而沒有樣式的狀況。
<!-- #else -->
聲明(意味着 css-loaded
的cookie 已經存在)用戶重複瀏覽。由於咱們能夠從某種程度上來假定,CSS文件以前已經被加載過了,咱們能夠利用瀏覽器緩存來提供樣式表。這樣從緩存裏面加載速度就很快了。一樣的方法也被用來在第一次異步加載字體,後續的重複瀏覽也是從緩存裏面獲取字體。
這就是咱們第一次和重複瀏覽時,咱們用來區分的cookies。
因爲咱們在重現頁面時很大程度上依賴於瀏覽器緩存,因此咱們須要確認咱們的緩存是否合理。理想中咱們是要永遠的存儲資源(CSS、js、 字體、圖片),只有當這些文件被修改時才須要更新。當請求的URL是惟一時,緩存就會失效。每更新一個版本,咱們都會用 git tag
打個標籤。因此最簡單的方式就是給咱們請求的URL加上一個參數(代碼版本號),如 https://www.voorhoede.nl/assets/css/main-8af99277a6.css?v=1.0.4.
。
可是,這種作法的缺點就是當咱們要寫一個新的博客post(這也是咱們代碼庫的一部分,並無永久地存儲在CMS),原來緩存的資源將會失效,儘管沒有改變原來那些資源。
就在咱們嘗試去改善這種方法時,咱們發現了 gulp-rev
和 gulp-rev-replace
。這些腳本會自動合理地在咱們的文件名稱後面添加一竄hash值。這意味着只有實際上文件被改變時,纔會去改變請求的URL,這樣每一個文件的緩存就會自動失效。這種作法讓我興奮不已啊!
若是你看到這裏,你應該是想要知道結果的。測試網頁的性能能夠採用像 PageSpeed Insights
這樣的工具,它有很實用的提示。也可使用 WebPagetest
來測試,有擴展性的網絡分析。我認爲測試網頁渲染性能的最好方法就是在瘋狂地遏制網絡通訊時來觀察網頁的進程。這意味着,用一種不切實際的方法來遏制通訊。在谷歌瀏覽器,你能夠這樣操做 (via the inspector > Network tab) 來限制通訊,觀察網頁造成過程當中,請求是如何緩慢加載的。
下面是咱們的網頁在 50KB/s 的狀況下的加載情況。
這是 de Voorhoede site
首屏的網絡分析,是網頁第一次進程的一個概覽。
注意到在50KB/s的網速中,咱們是如何讓首屏的渲染只用了 2.27 秒的。也就是第一張幻燈片和瀑布圖裏面的黃色線所表明的位置。黃線剛好繪在就是HTML已經加載完的時間位置。HTML包含了關鍵CSS,保證頁面的可觀性。全部其餘的CSS都是用懶加載,因此咱們能夠等到所有資源加載完時才與頁面進行交互。這也是咱們想要的效果!
另外一個值得注意的就是自定義字體歷來不在這緩慢的連接上加載。 font face
觀察器會自動注意到這一點。可是,若是咱們不異步加載字體,你注視大多數瀏覽器,都會出現 FOIT
(不可見文本的閃現,上文有說起)的狀況。
全部的CSS文件僅在8s後就都加載完畢。相反,若是咱們不採用加載關鍵CSS的方式,而是採用加載所有CSS,咱們在前8秒看到的將會是空白的頁面。
若是你感到好奇,想與那些不太注重性能的網站對比一下加載時間,那趕忙試試吧。那個時間確定是飛漲啊!
用上面介紹的工具測試了咱們的網站,結果也是讓人滿意的。 PageSpeed insights
在移動性能方面給了咱們100分,多麼了不得啊!
PageSpeed insights
對 voorhoede.nl
的測試結果! 速度100分!
當咱們查看 WebPagetest
時,咱們獲得下面這樣的結果:
WebPagetest 對 voorhoede.nl
的檢測結果
能夠看出,咱們的服務器運行良好,首屏的速度指標是693。 這意味着咱們的頁面在693毫秒後就能夠在寬屏纜線下被使用了。
咱們這樣還不算完成,還會不斷地重複咱們的方法。我不久的未來,咱們會主要關注如下內容:
目前咱們正在試驗HTTP/2。本文所描述的大多數東西都是基於 HTTP/1.1 權限內的最好實踐。簡言之,HTTP/1.1 要追述到1999年,那時 table佈局和行內樣式都如火如荼。HTTP/1.1 從沒爲 2.6MB的網頁要接受200個請求而設計。爲了減輕舊版協議帶來的痛楚,咱們結合JS、CSS、關鍵性CSS,還爲小圖片設置數據源URI等。用各類方法來節省請求。自從 HTTP/2 能夠在同一個TCP連接上平行地運行多個請求後,全部的這些聯結使用和減小請求的作法均可能成爲反面模式了。當咱們跑完這個實驗後,咱們將會採用 HTTP/2 協議。
這是一個在後臺運行的現代瀏覽器的 JavaScript API。它擁有不少特性,這些特性在之前的網站上都是沒有的,如離線支持、消息推送、背景同步等。咱們如今正嘗試用 Service Worker
, 但仍是得在咱們本身的網站上實現先。我向你保證,咱們會作到的!
所以,咱們想要本身控制和部署咱們的網站。並且如今咱們也要採用CDN來擺脫由服務端和客戶端實際距離所帶來的網絡問題。儘管咱們的用戶基本上都是荷蘭的,咱們也想向世界的前端社區反映咱們在質量、性能和推進網絡發展方面作的最好。
EOF
掘金是一個高質量的技術社區,從 ECMAScript 6 到 Vue.js,網站性能優化到開源類庫,讓你不錯過 Web 開發的每個技術乾貨。長按圖片二維碼識別或者各大應用市場搜索「掘金」,技術乾貨盡在掌握中。