網站前端調校之「禁止轉譯的 CSS」

Standard

網站前端載入校能優化上,可透過 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。

整理好想法,列一下作法:

  1. 把頭版中必要的 CSS 整理出來,以 <style> 標籤將樣式直接嵌入 <head> 中。
  2. 延後載入其餘的 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 資源。

這個自動化流程大概是這樣:

  1. 透過後端語言,建立了一個變數,可用來即時開啟或關閉 優化模式,差別在是否開啟「禁止轉譯 CSS 優化模式」。
  2. 透過 critical 解析 優化模式參數 關閉 時的 index.html,並透過其功能:
    • 自動偵測,將 頭版 中必要的 CSS 樣式存成 _criticalCSS.css
  3. 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 項工作:

  1. 清除上次產生的 Critical CSS 相關檔案
  2. 訪問本地端 Web Server 取得優化前的 index.html 並產生成檔案
  3. 使用 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
  });
});

參考文章

One thought on “網站前端調校之「禁止轉譯的 CSS」

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *