網站前端載入校能優化上,可透過 Google PageSpeed Insights 來評估與抓出問題,還會根據待改進項目的多寡及嚴重性給予優化上的分數。
最近在製作公司網站的專案,雖然沒什麼內容,但趁上線前就試著用 PageSpeed Insights 掃了一下。下圖是一開始的情況:
啊,雖然我不是分數控,但顯示紅色驚嘆號好像不大酷。
其中「啟用壓縮功能」及「使用瀏覽器快取功能」,只要調整 Web Server 的設定,啟用壓縮及快取功能即可。公司是用 Nginx,看來根本沒打開,我就隨手設定了一下(keyword:nginx gzip、cache)。但我非 Server 專業,就不班門弄斧囉,請自行參考相關說明文件。
而「清除前幾行內容中的禁止轉譯 JavaScript 和 CSS」這項中,列出了在目標網站的 <head>
標籤中、以 <link>
方式載入的 .css
檔案。雖然說明寫的是中文,但有種似懂非懂的感覺…
您的網頁含有 2 項禁止轉譯 CSS 資源,對網頁的轉譯作業造成延遲。
網頁的前幾行內容完全無法在下列資源載入完成之前轉譯。請延後載入或以非同步方式載入禁止轉譯資源,或是將這些資源的重要部分直接嵌入 HTML。
針對下列網址為 CSS 傳送進行最佳化處理:
http://xxxx/application.css
http://xxx/index.css
若情況允許,可直接採用 Google 的 PageSpeed 模組,相關的解決方案都「傳便便」了,這篇文章接下來的篇幅可能幫不上什麼忙啦,哈哈。
不過針對其他情況(Server 環境、職掌權限等等),或是像我 – 想靠自己實作解決這問題的小小前端,就繼續往下看吧 🙂
禁止轉譯的資源(Render Blocking Resources)
禁止轉譯到底是什麼呢?可能看一下原文比較容易了解。轉譯的原文是 render,但我覺得似乎翻作「繪製」比較讓人容易了解。(雖然這段過程的確要經過轉譯、建構再繪製,但這邊就不討論翻譯問題了 :P)
在此「禁止轉譯」便是網頁的繪製被阻塞(Render blocking)了。根據 Google Web Fundamentals 轉譯樹狀結構的建構、版面配置和繪製 此節的說明,必須具備 DOM 與 CSSOM 兩個樹狀結構,兩者結合後才能建構出「轉譯樹狀結構(render tree)」,進而將網頁呈現在使用者面前。
前者(DOM)來自 HTML(若沒內容要呈現什麼?)、後者(CSSOM)來自 CSS(若沒有樣式還能看嗎?有種沒穿衣服就被看到的感覺 XD 某些情況會造成這種不佳的體驗,稱為 FOUC (Flash of unstyled content))。所以,HTML 和 CSS 都是禁止轉譯的資源(Render Blocking Resources)。
著手優化
為了讓使用者能更快地看到網頁,我們必須針對這個部分優化。思路是這樣的:
假設我們的網頁內容頗長,使用者剛進來網站也只會看到網頁的頭版(Above-the-Fold,也就是最上方的畫面區域,在中國稱為「網頁首屏」)
那能不能只載入這個畫面中必要的這些 CSS 樣式就好?(而且這部份也不要再產生 HTTP request 了,直接把必要的 CSS 規則內嵌進去吧)讓那些頭版中不必要的 CSS 規則延後載入呢?
回去看看 PageSpeed Insights,也是給予這樣的建議:
網頁的前幾行內容完全無法在下列資源載入完成之前轉譯。請延後載入或以非同步方式載入禁止轉譯資源,或是將這些資源的重要部分直接嵌入 HTML。
整理好想法,列一下作法:
- 把頭版中必要的 CSS 整理出來,以
<style>
標籤將樣式直接嵌入<head>
中。 - 延後載入其餘的 CSS 資源。也許用 JavaScript 方式載入吧。
這裡列出 CSS Tricks 相關文章中提供的範例,比較能體會該怎麼做:
<html>
<head>
<link rel="stylesheet" href="things.css">
</head>
<body>
<div class="thing1">
Hello world, how goes it?
</div>
...
<div class="thing2">
Hey, I'm totally below-the-fold
</div>
</body>
</html>
<div class="thing1">
即在使用者一進網站能看到的頭版區域,<div class="thing2">
則在之外。
而 things.css
的內容為:
.thing1 { color: red; }
.thing2 { background: green; }
看來頭版中必要的只有 .thing1
,所以我們將其以 <style>
標籤嵌入內容的方式,抽放到 <head>
中:
<html>
<head>
<style>
.thing1 { color: red; }
</style>
</head>
<body>
<div class="thing1">
Hello world, how goes it?
</div>
...
最後再以 Filament Group’s loadCSS 提供非同步載入 CSS 的 JavaScript 載入其餘的 CSS 資源:
...
<div class="thing2">
Hey, I'm totally below-the-fold
</div>
<script>
/*!
Modified for brevity from https://github.com/filamentgroup/loadCSS
loadCSS: load a CSS file asynchronously.
[c][/c]2014 @scottjehl, Filament Group, Inc.
Licensed MIT
*/
function loadCSS(href){
var ss = window.document.createElement('link'),
ref = window.document.getElementsByTagName('head')[0];
ss.rel = 'stylesheet';
ss.href = href;
// temporarily, set media to something non-matching to ensure it'll
// fetch without blocking render
ss.media = 'only x';
ref.parentNode.insertBefore(ss, ref);
setTimeout( function(){
// set media back to `all` so that the stylesheet applies once it loads
ss.media = 'all';
},0);
}
loadCss('things.css');
</script>
<noscript>
<!-- Let's not assume anything -->
<link rel="stylesheet" href="things.css">
</noscript>
</body>
</html>
現在再去 PageSpeed Insights 測測看,「清除前幾行內容中的禁止轉譯 JavaScript 和 CSS」應該已經被解決囉!
秀一下我對公司網站優化後的測試結果吧!
自動化
不過要手動自己做這段嗎?既有那個正在維護的龐大網站怎麼辦?每次都自己手動來也太辛苦了!找了資料,發現還是有自動化的方法的!
這邊就不介紹 CSS Tricks 相關文章中透過 CSS 預處理器(例如 Compass 的 Jacket)處理的解決方案了,有興趣自己去閱讀吧。
接下來我的情境與作法。在公司網站的專案上(Server side 是基於 Python 的 Flask 框架),我透過 Gulp.js,加上 addyosmani 寫的 critical 來實作禁止轉譯 CSS 優化的自動化流程。
網站的首頁 index.html
,於 <head>
中以 <link>
標籤嵌入了名為 index.css
的 CSS 資源。
這個自動化流程大概是這樣:
- 透過後端語言,建立了一個變數,可用來即時開啟或關閉 優化模式,差別在是否開啟「禁止轉譯 CSS 優化模式」。
- 透過 critical 解析 優化模式參數 關閉 時的
index.html
,並透過其功能:- 自動偵測,將 頭版 中必要的 CSS 樣式存成
_criticalCSS.css
。
- 自動偵測,將 頭版 中必要的 CSS 樣式存成
- 在
index.html
中透過 Flask 的 Jinja2 樣板語言加入:- 偵測 優化模式參數 開啟 時:
- 嵌入內含
_criticalCSS.css
樣式的<style>
標籤到<head>
區塊中。 - 透過 JavaScript 非同步載入其餘的 CSS 資源。
- 加入
<noscript>
,以原方式 (<link>
) 載入index.css
。
- 嵌入內含
- 偵測 優化模式參數 關閉 時:
- 以原方式 (
<link>
) 載入index.css
。以便下次變更內容時還能取得完整 CSS 內容。
- 以原方式 (
- 偵測 優化模式參數 開啟 時:
針對「禁止轉譯 CSS」這項優化,目前我想的自動化流程差不多就是這樣,只是個 POC(Proof of Concept),還有不少「能改得更自動」的空間。未來有心得再更新上來 🙂
最後還是提一下,也是最重要的觀念,要好好了解自己寫的每一條 CSS,不斷改進 CSS 本身哦!放了一堆無用規則,就算做了本篇提到的優化流程,那也只是治標不治本呢。也許有些理想化,現實開發中可能沒這麼多時間與資源來貫徹、前端也不一定被重視(遠目),但仍要擁有這樣的信念,共勉之! 🙂
以下提供相關程式碼,有點小亂,就作為實作上的參考囉~
index.html
:(含 Jinja 樣板語言)
{% if critical_mode %}
<!-- **優化模式參數** 開啟 -->
<style>
{{ strCriticalCSS|safe }}
<!-- 內嵌 _criticalCSS.css 內容 -->
</style>
<noscript>
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
</noscript>
<!-- 延後載入完整 css -->
<script>
(function(u){function loadCSS(e,t,n){"use strict";function r(){for(var t,i=0;i<d.length;i++)d[i].href&&d[i].href.indexOf(e)>-1&&(t=!0);t?o.media=n||"all":setTimeout(r)}var o=window.document.createElement("link"),i=t||window.document.getElementsByTagName("script")[0],d=window.document.styleSheets;return o.rel="stylesheet",o.href=e,o.media="only x",i.parentNode.appendChild(o),r(),o}for(var i in u){loadCSS(u[i]);}}([
'{{ url_for("static", filename="css/index.css") }}']));
</script>
{% else %}
<!-- 關閉時以原方式載入 index.css -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
{% endif %}
gulpfile.js
:
引入相關 npm 套件,請自行安裝。
var gulp = require('gulp'),
....
critical = require('critical'),
http = require('http'),
fs = require('fs'),
argv = require('yargs').argv,
clean = require('gulp-clean'),
mkdirp = require('mkdirp');
定義 3 項工作:
- 清除上次產生的 Critical CSS 相關檔案
- 訪問本地端 Web Server 取得優化前的
index.html
並產生成檔案 - 使用 critical,透過工作 2 中產生的
index.html
解析產出_criticalCSS.css
。
// =====================================
// 清除既有產生的 _criticalCSS 相關檔案
// =====================================
gulp.task('cleanCriticalCSSDir', function () {
gulp.src('_criticalCSS/**.{html,css}').pipe(clean());
mkdirp("_criticalCSS", function (err) {
if (err) console.error(err)
});
});
// =====================================
// 訪問本地端 Web Server 以產生「優化前」的首頁內容 `index.html`(透過 no_critical 0 或 1 開啟或關閉 **優化模式參數**)
// =====================================
gulp.task('fetchIndex', ['cleanCriticalCSSDir'], function () {
if (!argv.url) {
console.error("Please enter the URL you wanna fetch... e.g., gulp fetchIndex --url http://localhost/?no_critical=1");
process.nextTick(function () {
process.exit(0);
});
}
try {
var file = fs.createWriteStream("_criticalCSS/index.html");
var request = http.get(argv.url, function(response) {
response.pipe(file);
});
} catch (e) {
console.error("When fetching index.html some error occurs... :(");
process.nextTick(function () {
process.exit(0);
});
}
return gulp.src('static/css/index.css')
.pipe(gulp.dest('_criticalCSS/static/css'))
});
// =====================================
// 解析 index.html 並產生 Critical CSS
// =====================================
gulp.task('criticalCSS', function () {
critical.generateInline({
// Your base directory
base: '_criticalCSS/',
// HTML source file
src: 'index.html',
// Your CSS Files (optional)
css: ['static/css/index.css'],
// Viewport width (例如可針對手機版偵測)
width: 1920,
// Viewport height (例如可針對手機版偵測)
height: 1080,
// Target for final HTML output
htmlTarget: 'index-critical.html',
// Target for generated critical-path CSS (which we inline)
styleTarget: '../static/css/_criticalCSS.css',
// Minify critical-path CSS when inlining
minify: true,
// Extract inlined styles from referenced stylesheets
extract: true
});
});
感謝分享!