Rails 網站效能提升實錄

我如何讓一個網站載入速度提升 173%

網站改造過後在 Google PageSpeed Insights 的分數
[2019/4/6 更新 1] 感謝 Yuna 來信提醒其實 image size也是一個可以被優化的面向,下面新增兩個 image compression 推薦資源。Thank you Yuna!

[2019/4/6 更新 2] 後來發現 CloudFlare CDN 有提供免費 CDN 服務,設定很快效果很好!

先上前後對照

Before vs After

https://jennycodes.herokuapp.com 改造前,55 分。
https://jennycodes.herokuapp.com 改造後,97 分

這是 Google PageSpeed Insights,一個幫你免費診斷網站效能的工具,直接輸入要測試的網址,它就會從資源使用效率、載入速度等給你一個評分,並且也會針對你的網站沒做好的部分給建議。

上面是動工前的樣子,可以看到幾乎每一欄位都是紅色,總分也只有 55,慘不忍睹。改造過後,總分變成 97 分,首次有效繪製(從輸入網址到看到頁面主要內容的時間)時間差了足足有 5.2–1.9 = 3.3 秒之多。在每豪秒都要斤斤計較的網路世界,3.3 秒大概可以換算成現實世界的三個世紀吧!

最近的事情忙到一個段落,終於有時間好好的整頓我用 Ruby on Rails 架的部落格 https://jennycodes.me。從今年初上線至今,最大的痛點就是網站載入速度很慢。(其實還有其他許許多多想要改進/實現的功能,但是我們一步一步來。)

網站慢到什麼程度呢?第一次進站大概需要等 5~6 秒,有時候等一等還會因為作業時間過久直接出現 application error 給你看(直接被伺服器放棄的意思)。

其實,本來就預期網站效能不會太好,因為這個網站是使用 Heroku 平台服務的免費層級,而它有兩點可能會拖慢網站的速度:

  1. 只要有超過三十分鐘的閒置時間,伺服器就會關掉。要再進入就會需要重新連線。
  2. 伺服器位置選項只有美國與歐洲,從台灣連過去就會需要比較久的時間。

儘管如此,網站的速度還是太不可思議的慢了。這畢竟是一個架構簡單(資料庫裡只有兩張資料表)的部落格,免費的平台也不應該這樣。

優化過程

這次網站改造主要做了三件事:

  1. Set config.assets.compile = false.
  2. Set all javascript files to async.
  3. Add jQuery Lazy plugin.
  4. Connect to CDN.

除了這四點,下面也會介紹一些這次沒有做但也可以大幅優化網站的項目。

讓我來一一解釋。

Set config.assets.compile = false

這個選項是在 config/environments/production.rb 中,預設值本來就是 false,heroku 官網也建議最好不要把這選項打開,因為打開的話靜態資源(asset)會在第一次請求到來時才被編譯並且快取,而在之後的請求伺服器都會經過一系列尋找、確認的步驟才會呈現,拖慢網站的效能。(這篇 stackoverflow 的解釋也值得一讀)。

但我的網站原本是將這個選項打開的,原因是在一開始上線時,不知道為什麼怎麼樣就是無法載入首頁背景的圖,試了很久之後,發現唯有將這個選項打開,然後再手動 precompile assets,圖片才可以成功載入。於是當初就決定先將就這個寫法,日後再來還債。

要還債,就不得不找出圖片不能載入的真正原因。要找到圖片不能載入的原因,就要先好好暸解 rails 的 asset pipeline 這個幫你處理靜態資源的機制。一個一個步驟檢視後,發現當發出 asset precompile 指令時,rails 找得到 css & javascript 檔案,但是照片檔卻有時找得到,有時找不到。進一步縮小問題,我的圖檔是 .jpeg,是檔案格式的問題嗎?

上網一查,果真如此。Rails 預設支援的圖片格式是 .jpg,所以 .jpeg 格式的檔案並不會自動編譯。在 config/application.rb 中加上 config.assets.precompile += %w(.jpeg),再試一次,果真成功了!

找出圖片無法載入的真正癥結,就可以放心調整config.assets.compile = true。但是還要記得清掉原本手動 precompile 在 public/ 資料夾產生的 assets 檔案,不然 heroku 在部署時看到這個檔案還是會直接拿來用,而不會更新資源。在 terminal 打 rake assets:clobber RAILS_ENV=production 就可以了。

Set All Javascript Files to Async

一個心頭大患解決了,但還是不夠好。 Google PageSpeed Insights 的診斷還有提供一些方向:

Google PageSpeed Insights 最初的診斷

第一條「排除禁止轉譯的資源」的解釋是「資源過多會妨礙首次繪製頁面。建議內嵌重要的 JS/CSS,延遲所有不重要的 JS/樣式。」意思是這些靜態資源其實也有親疏遠近之分;當你載入網頁時,有些 javascript 效果或是 css 樣式不那麼快出現也不會有人掛念,所以讓他們在比較重要的檔案完成載入之後再開始動作,就可以節省等待這些邊緣人被下載的時間。

要將不重要的 js 檔案延遲,可以在 template 引入 js 檔案的程式碼後面加上 async (asynchronous) ,例如 app/views/layout/application.html.slim(rails 預設的樣板引擎是 erb,我習慣用的是 slim)檔案,原本在 head 標籤中是

= javascript_include_tag ‘application’, ‘data-turbolinks-track’: ‘reload’

改成

= javascript_include_tag ‘application’, ‘data-turbolinks-track’: ‘reload’, async: Rails.env.production?

如此一來,application.js 這個 bundle 就會在 DOMContentLoaded 事件後才會執行。而這段程式碼的意思是「如果是在 production 環境的話,就讓此段 javascript 變成 async」。不直接讓 async = true 的原因是 development 環境的設定是 config.assets.debug = true。這個選項開啟後 rails 在執行應用程式時,就會將 js 引入檔一個個載入,而不會全部打包在一個大 js 檔案,如此讓除錯變得更加方便,但也會讓效能降低,所以只在開發模式開啟。

而在開發環境中, async = true 而 js 引入檔又都分別引入的情況下,所有檔案非同步執行的結果就是引入的順序會亂掉,就會出現像是 $ is not defined(沒有先載入 jQuery)的情況。

Turbolinks 與 Async 的微妙平衡

Turbolinks 是 一個 rails 自動載入的 gem,主要功能是「不完全換頁」,也就是在應用程式內切換頁面時,不重新載入所有的元素與樣式,可以節省的時間。讓你可以不使用前端 js 框架也可以享受到 SPA (Single Page Application) 的效果。

對 application.js 使用 async 後,必須注意原本寫在檔案內的 ‘turbolinks:load’ 事件會失效,因為現在 js 檔案會在頁面載入之後才會開始動作,也就是說當程式碼跑到 ‘turbolinks:load’ 的時候,頁面已經加載完了,所以這個事件就不會觸發,裡面的程式碼也就不會被執行。

我的做法是將 ‘turbolinks:load’ 觸發條件改成 ‘ready’

// app/assets/javascripts/application.js
// 原本是
// $(document).on('turbolinks:load', function() {
// console.log('I'm loading');
// });
// 改成
$(document).on('ready', function() {
console.log('I'm loading');
});

Turbolinks 的這個討論有提到一些其他的作法,比如說在 <head> 加一些程式碼或是使用 requestAnimationFrame,但是目前似乎還沒有一個完美的解法。

Connect to CDN

不得不說,這真的是個 CP 值很高的東西啊!CDN 全名是 Content Distributed Network,它的概念很簡單,前面不是說網站的伺服器是在美國嗎?CDN 會在全球各地設節點,然後把放在美國伺服器的網站內容複製到這些節點,當有人想要連線到網站,請求會被自動送到離這個人最近的節點,然後從那邊載入。我在《從點一個 URL 到看到頁面中間發生了什麼事?》裡面有比較詳細的介紹。

最棒的是,網路上提供 CDN 的公司—像是 CloudFlare—有提供免費的服務,而且設定超簡單,就不多介紹了,總之,還沒申請的快去吧!你不會後悔的。

Add jQuery Lazy Plugin

做到這邊,其實算是可以收手了(?)只是想到前公司專案有用過一個函式庫,讓網站上的圖片部分做到延遲載入( delayed content loading),其實跟 async 是相同的概念。考量到之後應該會在文章中放不少照片,到時候這個函式庫就會發揮很大效用,所以就先一鼓作氣,順道解決。

其實加入這個也不難,就是依照網站指示做就好了。唯一一點需要注意的是如果你的 jQuery 是在 application.js 中,那 jQuery Lazy 也要下載放在專案裡面,不能直接從 CDN 載入,不然使用 async 後就會出現上面提到的, 載入順序亂掉導致出現錯誤。

更上一層樓:Compress Your Images

當網站規模逐漸擴大,除了想辦法提升應用程式本身的效能之外,還可以檢視使用資源本身大小,像是如果是有大量使用照片的話,則在匯入時就先處理過照片就會顯著提升載入效能。這裡 Yuna 推薦網站 Websiteplanet’s ImageCompressor,操作簡單到你只要把圖片拉近框框裡面就好了。隨便丟了一張圖片來測,原本 676KB 的照片被壓縮到只剩下 179 KB,不錯。

儘管它已經很方便了,如果不想要每次上傳都要去先去網站手動壓縮的話,可以考慮 ImageOptim 這個服務。它同樣是免費的,但強大的地方在它直接是 gem 的形式—也就是說你可以用程式碼自動壓縮傳進來的圖片,非常厲害!

更進一步還有各式各樣的快取,例如 Memcached, Redis 等(其實 CDN 也是快取的一種)等你去玩,不過對於一個如此規模的部落格而言,現在這樣足夠了。所以就暫且告一段落,繼續往下個目標前進。

參考資料

PageSpeed Insights
developers.google.com
Ruby on Rails Gotcha: Asynchronous loading of Javascript in development mode
rhardih.io
turbolinks:load and async script loading · Issue #28 · turbolinks/turbolinks
github.com
If page is interactive, fire pageLoaded() immediately by nateberkopec · Pull Request #274 ·…
github.com

http://jquery.eisbehr.de/lazy

文章同步發表於 Medium


  • Find me at