2.前端效能分析
要做效能最佳化,首先需要知道怎麽科學化量測。需要數據支持,我們才可以知道最佳化有沒有效果。
Chrome 除錯器提供了詳盡的報表,最基本我們可以觀察 Network,其中的 Load 就是 Page Load Time 頁面加載時間:
對前端來說,看重的是 Page Load Time,而不是單一 Request 的時間。因為網頁的組成除了 HTML 之外,還有 CSS/JavaScript/圖片等等。
Chrome 也提供了進階的 Performance 詳細報告,包括解析 HTML、CSS、JavaScript 各花了多少時間,等會也會介紹如何使用:
Google 也提供了一些分析工具,並提供分數報告,包括:
線上分析工具 PageSpeed Insights
PageSpeed Insights 的評分標準,基本上就是本教程會說明的內容
Chrome 外掛 Lighthouse
稍後的作業會請各位安裝這個 Lighthouse 工具。
模擬低網速
由於開發者的網路線速度通常蠻快的,不能反應實際用戶的連線情況,特別是用手機行動上網的情境,因此 Chrome 有提供模擬限速的功能。 在截圖中的 No throttling 的地方,我們可以將速度模擬成行動 3G 網路。另外在測試的時候,也需要把瀏覽器的快取功能關閉。
3. 減少 Requests 數量和大小
要完整加載一個網頁,除了 HTML 之外,還會需要加載 CSS、JavaScript 和圖片等資源。每一次的 HTTP Request 都需要耗費時間,因此減少 Requests 的請求數量,以及減少這些檔案的大小,就是一個最佳化的方向。
Rails 在本機開發時,CSS 和 JavaScript 是分開加載的:
在部署上 Production 環境時,Asset Pipeline 會執行 rake assets:precompile
將 CSS 和 Javascript 進行合並壓縮,變成這個樣子:
仔細觀察 CSS 內容壓縮後會變成這樣:
JavaScript 內容壓縮後會變成這樣:
CSS 壓縮是透過 sass-rails gem,而 JavaScript 壓縮是透過 uglifier gem
本機如何跑 production 模式?
也因為這個原因,在本機用 development 模式進行前端效能測試是不準的。如果想在本機跑 production 模式的話,可以這樣做:
- 修改
config/database.yml
設定 production 模式要用哪一個資料庫,例如把 production 改成用 development 模式的資料庫,例如
development:
<<: *default
database: db/development.sqlite3
production:
<<: *default
- database: db/production.sqlite3
+ database: db/development.sqlite3
-
執行
bundle exec rake assets:precompile
,這會在本機執行 Asset Pipleline 的編譯 -
暫時修改
config/environments/production.rb
打開public_file_server
,這會允許 Rails 伺服器可以回傳靜態檔案(在正式部署的伺服器上,靜態檔案會由 Nginx 處理,所以預設是關閉的)
- config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+ config.public_file_server.enabled = true
-
改用
rails s -e production
來啟動 Rails,就會是用 production 模式 -
Asset Pipleline 的編譯出來的檔案放在
public/assets
下,這些檔案是不需要 commit 的,實驗完之後需要砍掉。執行bundle exec rake assets:clobber
或rm -rf public/assets
可以砍掉這個目錄。
不過在 production 模式下,修改任何 Ruby 代碼,都需要重啟 Rails。修改任何前端代碼,都需要重新編譯 Asset Pipleline,是很麻煩的。
4. HTTP 最佳化
HTTP 通訊協議本身,也有一些可以提速的功能:
HTTP 壓縮
網站伺服器和瀏覽器之間,支援了 gzip 壓縮,我們在部署教程中,就有設定 Nginx 打開這個功能:
gzip on;
gzip_disable "msie6";
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/xml text/plain text/javascript text/x-component;
一旦啟用這個功能,Nginx 就會針對這些純文字的檔案,進行 gzip 壓縮之後再傳給瀏覽器,在 HTTP Response 標頭上會標明這個有 gzip 壓縮,那麽瀏覽器就會知道要進行解壓縮
檔案有沒有壓縮差別很大,截圖中 JavaScript 壓縮前是 1.1 MB,壓縮後是 322KB,CSS 壓縮前是 142KB,壓縮後是 22.7KB。所以這個功能務必在伺服器上要開啟。
圖片不需要 gzip 壓縮,因為圖片本身已經是壓縮過後的格式(PNG或JPG)
HTTP 快取
在 HTTP 標頭中,也有定義一些快取功能。我們之前在部署教程中,在 Nginx 伺服器有這樣的設定:
location ~ ^/assets/ {
expires 1y;
add_header Cache-Control public;
add_header ETag "";
break;
}
這會針對 /assets/
目錄下的靜態檔案,在 HTTP Response 加入以下的 Headers:
cache-control: public
指定這個資源是可以被瀏覽器快取cache-control: max-age=31536000
和expire
告訴瀏覽器這個資源可以被快取多久
於是瀏覽器就知道這些資源可以被快取,當你重新整理頁面,或是瀏覽下一頁的時候:
這些資源就從瀏覽器的快取拿出來,而不需要發出 HTTP Requests 到伺服器。
為什麽可以將快取時間拉到一年這麽久呢? 這是因為 Rails asset pipleline 的 digest 功能會修改檔名加上編碼。每次部署上線進行 rake assets:precompile
編譯時,如果 CSS/JS/圖片 內容有變動的話,那這個檔名就會變,瀏覽器就會下載新的檔案。因此我們可以放心告訴瀏覽器這些資源可以被快取,而不用擔心用戶繼續使用舊的檔案。
HTTP/2
HTTP/2 是 2015 年制定的最新 HTTP 標準,語意和功能都與 HTTP 1.1 是相容的,主要是改善加載網頁的效能,詳細請參考老師之前寫的一篇文章 更快更安全: 每個網站都應該升級到 HTTP/2。
HTTP/2 在圖片很多的場景,使用 HTTP/2 會改善非常多。不過 HTTP/2 要求 HTTPS,這是部署上會比較麻煩的地方。
HTTPS 安全宣導:自 2017 年 10 月起,如果用戶使用 Chrome (62版) 時,在使用 HTTP 的網頁表單中輸入文字,Chrome 將顯示「不安全」警告
5. 關鍵渲染路徑
除了網頁加載時間(Page Load Time)之外,另一個前端效能註重的數據是首次渲染頁面的時間。
首先我們來瞭解一下瀏覽器渲染畫面(Browser Rendering)的過程:
- 瀏覽器下載 HTML
- 瀏覽器針對 HTML 上的外部資源(CSS/JavaScript/圖片)發送 HTTP Requests 去獲取,在 HTTP 1.1 時代只能同時抓取六個資源、在 HTTP/2 之後則可以同時平行抓取。
- 同一時間瀏覽器也從 HTML 源碼上到下開始解析進行渲染:
- 用 HTML 建立 Document Object Model (DOM)
- 用 CSS 建立 CSS Object Model (CSSOM),如果 CSS 還沒下載完成,會等待
- 執行 JavaScript 來操作 DOM 和 CSSOM,如果 JavaScript 還沒下載完成,會等待
- 建立 Render Tree
- 計算每個元素在畫面上的位置 Layout
- 實際畫上(Paint)每一畫素(pixels)
這個過程其實可以是一個漸進的過程,我們希望盡快讓用戶看到有個畫面,也就是去縮短首次渲染頁面的時間,而不是完全的空白畫面。因此希望找出所謂的關鍵渲染路徑(Critical Rendering Path):
這張圖出自 Google 文檔,上圖是經過關鍵渲染路徑優化的版本、下圖是沒有經過優化的版本。你可以看到經過優化的版本,用戶可以更早就看到畫面,在感受上可以更早就開始閱讀和操作網頁。
這要怎麽辦到呢? 關鍵就在思考首次渲染頁面時,只加載必要的 CSS,以及延後 JavaScript 加載。這是因為顯示畫面只需要 HTML/CSS,暫時不需要 JavaScript。
如何量測?
讓我們用 Chrome 除錯器的 Performance 功能,可以觀察加載的看時間軸:
首先限速一下,這樣比較好觀察效果:
點到 Performance tab,打開 Screenshots,按下重新整理就會開始量測:
首先讓我們測試一個沒有優化的版本 https://www.rails-recipes.win,你會發現要到 3.5 秒的時候,第一個畫面才會出來:
接來下測試一個優化之後的版本 https://www.rails-recipes.win/?js=async,你會發現大約 1.6 秒時,第一個畫面就出來了:
此範例網站 www.rails-recipes.win 已關站
這是怎麽辦到的,最重要的技巧是延後 JavaScript 的加載:
JavaScript 加載最佳化
JavaScript 的加載對瀏覽器來說是 rendering blocking 的,當瀏覽器在 <head>
裡面看到 <script>
標籤時,瀏覽器會等待下載完成,並執行這個 JavasSript 後,才會繼續 rendering HTML 畫面。
圖例中綠色是 HTML 渲染、紫色是 JavaScript 下載、紅色是 JavaScript 執行。瀏覽器看到 <script>
會暫停 HTML 渲染,來下載和執行 JavaScript。
在 Rails 的 layout/application.html.erb
中,JavaScript 就被放在 <head>
裡面,優化的辦法有幾種:
傳統做法:將 script 移到底部
傳統做法是將 <script>
的加載移出 <head>
,放在 HTML 底部,</body>
上一行。例如:
....
+ <%= javascript_include_tag 'application' %>
</body>
這個做法的優點是所有瀏覽器都支援,但是缺點是與 Rails 的 Turbolinks 功能是沖突不相容的(請復習 “Ajax 互動式網頁應用” 教程 “3-1 Turbolinks 坑”),因為 Turbolinks 的作用就是保留 <head>
用 Ajax 替換 <body>
區塊,如果把 JavaScript 移出 <head>
那 Turbolinks 就會不正常運作。
新標準做法:async 和 defer 屬性
新的瀏覽器標準可以透過 async 或 defer 屬性,告訴瀏覽器這個 JavaScript 加載可以是非同步的,不要阻擋 HTML 的 rendering。
修改 app/views/layout/application.html.erb
- <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload', :async => true %>
或是
- <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
+ <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload', :defer => true %>
async 和 defer 的差別是:
async 的話,瀏覽器不會阻擋 HTML 的渲染,當 JavaScript 下載完成時就會直接執行 JavaScript,不會等 HTML DOM 加載。
defer 的話,瀏覽器也不會阻擋 HTML 的渲染,但是當 JavaScript 下載完成後,會等 HTML 加載完成,才會執行 JavaScript。如果 JavaScript 裡面有依賴 DOM 的話,適合用這個方式。
async 和 defer 是瀏覽器的新標準,優點是可以比傳統做法效果更好。但是缺點是舊的瀏覽器支援不好,IE<=9 的版本不支援。
Inline 型式的 JavaScript 問題
無論是底部或 async/defer 做法,JavaScript 執行的順序都需要註意不然會出錯。之前在實戰應用章節中都假設 JavaScript 是放在 head,因此放在 HTML 內的 JavaScript 會出錯。例如在 app/views/events/_form.html.erb
中,我們在 HTML body 裡面寫了:
<script>
$("#event_category_id").select2( { theme: "bootstrap"} );
</script>
但是這段代碼無論是用 JavaSript 改放在下方,或是用 async/defer 的方法,都會因為找不到 jQuery 的 $
而出錯,因為它的執行順序跑在 jQuery 加載之前了 :(
那要怎麽調整呢?
若是採用 script 在底部的調整方式:
解決方式是我們在 javascript_include_tag
下再多加一行 yield :custom_javascript
,這會在 layout 樣板中先佔一個位置。
<%= javascript_include_tag %>
+ <%= yield :custom_javascript %>
</body>
然後再將 HTML body 裡面的 JavaScript 代碼,用 content_for
包起來。那麽這一段 script 最後顯示的時候,就會塞到上述的 yield 位置裡面,也就是會在javascript_include_tag
之後才被執行了。例如:
<%= content_for :custom_javascript %>
<script>
$("#event_category_id").select2( { theme: "bootstrap"} );
</script>
<% end %>
若是採用 defer 的調整方式:
將原本寫的 JavaScript 代碼,延後到 DOMContentLoaded 事件後才觸發:
例如實戰應用中的 app/views/admin/events/_form.html.erb
本來有一段使用 select2 的代碼:
<script>
+ window.addEventListener('DOMContentLoaded', function() {
$("#event_category_id").select2( { theme: "bootstrap"} );
+ })
</script>
async 的調整方式:
HTML 中不能有 inline 形式的 JavaScript 了,因為我們不知道那些 async 的 JavaScript 到底什麽時候會被加載,因此所有代碼都必須放在打包後的 application.js
中。請將把 layout 的 <body>
改成 <body id="<%= "#{controller_name}-#{action_name}"%>">
,這樣就可以在全局載入的 application.js
中指定只有這一頁才執行的js code,例如:
$(document).ready(function(){
if ( $("#events-edit").length > 0 ) {
$("#event_category_id").select2( { theme: "bootstrap"} );
}
})
實地測試
由於需要用 Rails 的 Production 模式才能正確的測試效果,為了方便大家實地看看效果,在以下網址提供了不同做法,你可以用 Chrome 除錯器的 Performacne 功能測試看看:
- https://www.rails-recipes.win/ JS 放 head
- https://www.rails-recipes.win/?js=bottom JS 放底部
- https://www.rails-recipes.win/?js=async 用 async 加載
- https://www.rails-recipes.win/?js=defer 用 defer 加載
此範例網站 www.rails-recipes.win 已關站,程式碼在 https://github.com/ihower/rails-recipes/blob/done/app/views/layouts/application.html.erb 用不同
params[:js]
參數決定 js 位置來做實驗。
CSS 加載最佳化
CSS 也被瀏覽器視為一種 render blocking 的資源,當瀏覽器解析 HTML 看到 <link href="style.css" rel="stylesheet">
時,就會等待完整解析這個 CSS 後,才會繼續渲染畫面。
要渲染 HTML 畫面,加載 CSS 是必要的,但是 Rails 預設的 application.css
是將全部的 CSS 打包,我們可以拆出一些關鍵的 CSS 包成一個檔案,這個檔案比較小,因此可以加速首次渲染頁面的時間。然後將其他沒這個重要的 CSS 包在另一個檔案,透過非同步加載的方式。詳細可以參考這篇的做法 Optimize CSS delivery in Rails app 。
6. CDN
前面我們提到伺服器如果離用戶比較近,網路傳輸的時間就會比較快。不過如果用戶散佈是世界各地的話,那麽伺服器放哪裡都會有人距離比較遠,這種情況怎麽辦呢?
這時候我們就會使用 CDN (Content delivery network) 這種網路服務來加速靜態檔案的下載。
CDN 是專門用來提供靜態檔案的伺服器,服務商在各大城市都有部署 CDN 伺服器,因此用戶會從距離最近的 CDN 伺服器下載靜態檔案,如果 CDN 上面沒有需要的檔案,那麽 CDN 會從我們的伺服器上下載一份回去快取起來。
例如你的伺服器放在日本,但是台北有 CDN 節點,於是用戶需要的圖片就會從台北的 CDN 下載。只有靜態檔案例如 CSS/JavaScript和圖片等會被快取放在 CDN 上,如果是動態產生的內容,用戶就會要訪問我們自己的伺服器了。
首先我們需要將 HTML 網頁上靜態檔案網址分開,改成 CDN 的網址:
- 例如伺服器網站是
www.jd.com
,你會從這個網址先下載 HTML - 但是 HTML 上的圖片網址則是不同的,例如是
cdn.jd.com
好了,這個是 CDN 的網址 - CDN 技術做的是針對不同地區的用戶,自動提供他們最近的點下載檔案
- 不同地區的用戶,針對
cdn.jd.com
會解析出不同的 IP 地址,選擇用最近的伺服器 - 如果 CDN 上有檔案快取,就直接從 CDN 上吐給用戶
- 如果 CDN 上沒有檔案快取,就從原站拉一份,快取在 CDN 上
- 不同地區的用戶,針對
如何在 Rails 上實現
你不需要去改網站上的 image_tag 一個一個處理。
只要在 Rails 上改全站的圖片來源,只要修改 config/enviorments/production.rb
里的這一行就可以了。
config.action_controller.asset_host = "https://cdn.jd.com"
這樣全站的 image/css/js 網址,就會全部變成
- cdn.jd.com/images/demo.jpg
- cdn.jd.com/assets/admin.css
- cdn.jd.com/assets/admin.js
在哪可以找到 CDN 服務
上述服務在中國境內沒有節點,中國境內請用以下(但你的網站需要有ICP備案才能申請使用)
預熱網路連接(Resource Hint)
本節尚未完成
https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf https://www.keycdn.com/blog/resource-hints/ https://css-tricks.com/prefetching-preloading-prebrowsing/
使用頁面框架,對高消耗組建延遲加載(字體,JS文件,循環播放,視頻和內嵌框架)。使用資源提示來節省在dns-prefetch(指的是在後台執行DNS檢索),preconnect(指要求瀏覽器在後台進行握手鏈接(DNS,TCP,TLS)),prerender(指要求瀏覽器在後台對特定頁面進行渲染)
preload(指的是提前取出暫不執行的源文件)。根據你瀏覽器的支持情況,盡量使用preconnect來代替dns-prefetch,而且在使用prefetch和prerender要特別小心——後者(prerender)只有在你非常確信用戶下一步操作的情況下再去使用(比如購買流程中)。
進一步減少 Requests 數量和大小
清理 CSS
本節尚未完成
CSS 寫多了,很可能有些 CSS 根本沒有用到,以下工具可以幫我們檢查清理沒有用的 CSS
壓縮圖片
本節尚未完成
網頁上的圖片也需要有適當的大小和壓縮
這也是為什麽用 carrywave 時,上傳圖片時,會先壓縮成不同大小
另外,常見的 JPG 和 PNG 都已經歷史悠久
Google 最新推出的 webp 格式更棒,只是只有 Chrome 支援。
Lazy Load 圖片
本節尚未完成
讓在可視範圍外的圖片不加載,等到即將進入可視範圍時才進行加載。透過這個方法可讓資源請求分散,而不是集中在一起,且如果使用者還沒瀏覽到該處就離開網站,也可以達到減少請求的目的。
https://github.com/aFarkas/lazysizes
參考資料
- Google 的效能指南以及PageSpeed Insights 的評分標準
- https://www.railsspeed.com
- https://hackernoon.com/optimising-the-front-end-for-the-browser-f2f51a29c572
- https://developers.google.com/web/fundamentals/performance/critical-rendering-path/
- https://varvy.com/pagespeed/critical-render-path.html
- https://www.w3ctech.com/topic/1945 翻譯
- https://www.smashingmagazine.com/2016/12/front-end-performance-checklist-2017-pdf-pages/ 原文英文,這很雜,但很 cutting-edge
- https://hackernoon.com/optimising-the-front-end-for-the-browser-f2f51a29c572 解釋不錯
- https://medium.com/@Negaihoshi/前端性能優化與-seo-e7173108088f