Perl 之父 Larry Wall 曾說過,工程師有三大美德,分別是:「懶惰」、「急躁」與「傲慢」。其中的「急躁」指的便是對於效能低落的程式會有所追求。

身為一個前端工程師,如果做出來的網站一打開就白畫面好幾秒,點擊連結後就開始無止盡的等待,畫面樣式變動還會一格一格卡卡的,無論是誰都會受不了。對於這些情況,能夠怎麼優化、改善?

尋找問題#

要進行優化改善,首先就是要能夠辨識出問題可能在哪裡。先思考一下,使用者所看到的網頁內容是怎麼來的。

首先必然是下載內容。現代的網站為了更容易吸引使用者的眼球,除了豐富的文本內容外,還會使用大量的圖片、影片傳達資訊,甚至透過聲音、動畫等方式與使用者做互動,這些多媒體內容,都代表著更多的多媒體資源以及程式碼。

取得網頁內容後,瀏覽器便開始解析程式碼,並渲染出畫面。

綜合以上,前端工程師較容易實現的網站優化項目,會集中在「載入資源」及「畫面渲染」這兩部分。

優化載入#

現代網站中需要下載的內容以驚人的速度瘋狂成長,現在的網站平均需要下載數 MB 之多的資源。這麼巨量的內容,中間也自然會有許多可以進行優化的地方。

減少資源量#

最直覺的就是減少下載的內容。

程式碼的部份,可以將換行符號、無意義的空白、註解等方便人類看的內容移除,把程式內容最小化(minify)。例如以下的 CSS:

p.round {
  border: 2px solid red;
  border-radius: 12px;
}

經過最小化處理後會變成:

p.round{border:2px solid red;border-radius:12px}

同樣的道理也可以套用在 HTML 及 JavaScript 上。例如說明閉包時用的範例程式碼:

function loadPicture() {
  let count = this.pageContent.length;
  const load = (target, resolve) => {
    const counter = () => (--count ? count : resolve());
    let img = new Image();
    img.onload = counter;
    img.onerror = counter;
    img.src = target.url;
  };
  return new Promise((resolve) => {
    if (!count) return resolve();
    this.pageContent.forEach((target) => load(target, resolve));
  });
}

同樣經過最小化處理後會變成:

function loadPicture() {
  let count = this.pageContent.length;
  const load = (target, resolve) => {
    const counter = () => (--count ? count : resolve());
    let img = new Image();
    img.onload = counter;
    img.onerror = counter;
    img.src = target.url;
  };
  return new Promise((resolve) => {
    if (!count) return resolve();
    this.pageContent.forEach((target) => load(target, resolve));
  });
}

由於所有的換行、縮排對瀏覽器解析內容來說都是沒有意義的部分,將它們移除不但不會影響到功能,還可以節省大量的傳輸量。

另外,JavaScript 的變數名稱,除非是留給其他程式呼叫,否則對瀏覽器來說也是無意義的,還可以將程式碼醜化(uglify),把變數名稱都換成更精簡的內容:

function loadPicture() {
  let e = this.pageContent.length;
  return new Promise((n) => {
    if (!e) return n();
    this.pageContent.forEach((t) =>
      ((n, t) => {
        const r = () => (--e ? e : t());
        let o = new Image();
        (o.onload = r), (o.onerror = r), (o.src = n.url);
      })(t, n)
    );
  });
}

這段程式碼壓縮到最後,總共減少了 46.48% 的大小。

除了程式之外,多媒體的內容大小也是另一個重點:

  • 小動畫,與其用 Gif,不如換成 MP4 等影片格式,整體的影片品質不但比較好,所佔容量也會下降
  • 圖檔的部分,最基本的就是不要用超過顯示尺寸的圖
  • 大部分的圖檔在網頁中顯示時,可以犧牲一部分的品質細節,換來更小的檔案容量

只拿必要的檔案#

把無意義的內容都拿掉了,剩下就全都是這個網站需要的內容了嗎?其實不然。例如造訪一次 Instagram 或 Facebook,可能只會從眾多的功能之中使用一小部分,並不會全部功能都使用。如果可以在使用者需要的時候,才將必要的資源提供給使用者,就可以進一步減少傳輸內容的數量。

在程式碼的部份,可以將整個網站共用的部分,以及彼此沒有相關聯的程式分成幾個大部分(chunk),在使用者造訪指定頁面時,只提供該頁面所需要的部分。例如一個網站有 10 個頁面,使用者看 A 頁面時給予 A 頁面所有需要的部分,看 B 頁面時也僅須傳遞 B 頁面的部份,其他未造訪、對使用者來說無意義的頁面內容,也就不用浪費資源傳遞。這樣的解決方案,在前端 SPA(Single Page Application)中非常常見。

再仔細切分下去,使用者即使看了這個頁面,其實也不一定會整頁看完。因此其實也不需要把整個頁面內的內容全都載下來。例如 Facebook 的動態牆,不會一口氣就載入個 50、100 則近況更新,而是在向下滑動閱覽的同時,持續載入更多新的內容。

例如佔流量最大宗的圖檔,可以透過 Web API 的 Intersection Observer 建立一個觀察者,當圖檔區塊即將進入視窗時,替換掉圖檔的 src 屬性,達到圖片延遲載入(lazy loading)的目的。

最後,再更進一步的思考,頁面中總有些資料是不太會變動的,例如城市區域選單、郵遞區號選單的資料等等,如果使用者以前就曾經來過這個頁面,是不是可以讓使用者不要再次下載?

這就是快取(Cache)的機制,除了瀏覽器本身的快取機制外,如果真的必要,也可以透過 Web Storage,自行建立固定資料的儲存機制。

優化渲染#

優化完下載的部分,接著是渲染的優化。常見的方法包含:

  • 一般的 <script> 載入資源時會阻礙畫面渲染,應該要加上 deferasync 屬性
  • 盡量使用樣式屬性的變動來取代排版屬性的變動,避免造成 Reflow
  • 注意圖層數量,過多時也會影響效能

除此之外,還有一個比較少提到的,就是要讓出 JavaScript 的主執行緒(Main Thread)。由於 JavaScript 是單執行緒的程式語言,現代網站也往往會利用 JavaScript 做一些複雜的視覺處理,當有複雜的運算卡住主執行緒時,需要由程式觸發的畫面效果便會無法正確呈現。解決方法的部分,可以利用 Web Worker 代為處理較複雜且長時間的計算。

關於用程式觸發動畫,請記得用 requestAnimationFrame 取代 setTimeoutsetInterval;後兩者因為 Event Loop 的緣故,執行的時間並不準確,動畫可能會造成跳格、延遲等效果。

結語#

優化網頁對前端來說是非常重要的一環,也是影響使用者體驗最大的關鍵。從優化載入與優化渲染兩個方向出發,逐步探討在瀏覽器解析網頁的各個階段中,程式碼是如何影響解析的過程及結果。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10225482