四種加快網站開啟速度的常見方法 延遲載入、程式拆分、打包檔分析、資源優化

me
林彥成
2021-03-09 | 4 min.
文章目錄
  1. 1. 四種加快網站開啟速度的常見方法
    1. 1.1. 延遲載入資源 (Lazy Load)
      1. 1.1.1. Script 載入方式 (async, defer, lazyload)
  2. 2. 三種方法 (async, defer, lazyload)
  3. 3. 使用程式懶載入
    1. 3.1. 程式拆分 (Code Splitting)
    2. 3.2. 打包檔分析器 (Bundle Analyzer)
    3. 3.3. 資源優化 (Resource Optimization)
  4. 4. 網站速度測試工具

四種加快網站開啟速度的常見方法

這篇文章會簡介加快網站開啟速度的四種常見方法,透過延遲載入資源、程式拆分、打包檔分析、資源優化幾個項目來作優化,並且用網站速度測試工具來進行檢核是否有效。

網站在載入效能上會遇到哪些問題?

  1. 靜態資源過多載入過慢
  2. 腳本複雜執行時間過長
  3. RWD 的設計造成樣式檔越來越大包

延遲載入資源 (Lazy Load)

圖片的部份 npm 上有個套件叫 lazyload,用的招式其實就是修改了 img 圖片來源的 attribute,並透過觀察的方式把 attribute 換回來進行 lazyload。

<img src="<image-url>" width=400 height=400>
<img class="lazyload" data-src="<image-url>" width=400 height=400>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// default settings
const defaults = {
src: "data-src",
srcset: "data-srcset",
selector: ".lazyload",
root: null,
rootMargin: "0px",
threshold: 0
};
this.images = document.querySelectorAll(this.settings.selector);
---

let observerConfig = {
root: this.settings.root,
rootMargin: this.settings.rootMargin,
threshold: [this.settings.threshold]
};

this.observer = new IntersectionObserver(function(entries) {
Array.prototype.forEach.call(entries, function (entry) {
if (entry.isIntersecting) {
self.observer.unobserve(entry.target);
let src = entry.target.getAttribute(self.settings.src);
let srcset = entry.target.getAttribute(self.settings.srcset);
if ("img" === entry.target.tagName.toLowerCase()) {
if (src) {
entry.target.src = src;
}
if (srcset) {
entry.target.srcset = srcset;
}
} else {
entry.target.style.backgroundImage = "url(" + src + ")";
}
}
});
}, observerConfig);

Array.prototype.forEach.call(this.images, function (image) {
self.observer.observe(image);
});

Script 載入方式 (async, defer, lazyload)

HTML5 <script> tag 中有多 async 和 defer 這兩個 attribute 可以使用,透過這兩個 attribute 我們可以非同步的去載入腳本,來加快網頁載入的速度。

三種方法 (async, defer, lazyload)

  • async: 如果可以就非同步,不保證按照順序,若沒有相依性且拿來操作畫面,是最佳做法
  • defer: 頁面載入後都執行完才按照順序執行,可用來加速網頁載入
  • lazyload 用程式控制載入時機

async 按照 MDN 文件 說明在 inline 的狀況不會有效果,必須帶有 src 屬性。:

1
2
3
<script async>
var code;
</script>
1
2
3
var script = document.createElement("script");
script.innerHTML = "code";
document.body.appendChild(script);

使用程式懶載入

透過程式來 lazyload 腳本,不想用新潮寫法的話透過以下簡單寫法就可以延後載入非必要的腳本,譬如在我部落格中的 diqus 就是透過這樣的方法進行 lazyload 的,我會偵測是否有捲動到某個特殊的 div 如果有才進行載入。

1
2
3
4
5
6
7
8
9
10
11
12
function loadScript(src, callback) {
let script = document.createElement("script");
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}

loadScript("/script.js", function (script) {
loadScript("/script2.js", function (script) {
loadScript("/script3.js", function (script) {});
});
});

Promise 的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function newScript() {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "./test.min.js";
script.addEventListener("load", () => {
resolve();
});
script.addEventListener("error", (e) => {
reject(e);
});
document.body.appendChild(script);
});
}
newScript()
.then(() => {
self.setState({ status: true });
})
.catch(() => {
self.setState({ status: false });
});

Google Tag Manager 的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
(function (a, b, c, d, e) {
a[d] = a[d] || [];
a[d].push({
"gtm.start": new Date().getTime(),
event: "gtm.js",
});
var f = b.getElementsByTagName(c)[0],
g = b.createElement(c),
h = "dataLayer" != d ? "&l=" + d : "";
g.async = true;
g.src = "https://www.googletagmanager.com/gtm.js?id=" + e + h;
f.parentNode.insertBefore(g, f);
})(window, document, "script", "dataLayer", "GTM-XXXXXXX");

程式拆分 (Code Splitting)

JavaScript 在早期伺服器渲染的階段,檔案較小且大多只有擔任簡單的事件互動,可以說幾乎沒有優化的需求,近幾年 Node.js 的快速發展加上 SPA 的概念出現後,才開始慢慢出現比較大且複雜的腳本。

當腳本複雜化後就開始有了 module 的概念,方便在撰寫的時候重複組合使用,在 JavaScript 中的 module 有很多種載入方式,像是早期的 CommonJS 或 AMD-based 的 RequireJS 或是都支援的 UMD,再來就是用 Webpack 搭配 Babel 直接用 ES6 的寫法。

Webpack 這類的工具起初的目標是幫我們把拆分的 module 做好打包,方便我們用 bundle 後的檔案進行發佈,當腳本開始變多時就會有越來越多當下不需要但又會被載入的部分,隨後應運而生的就是 code splitting 的概念。

Code splitting 如何去優化網站的載入效能?
因為腳本執行與樣式渲染都會影響網站的載入,原則上就是透過是當的分類拆分,透過 lazyload 的概念來達到優化的目的。Code splitting 能夠做到的應用場景如下:

  1. 按照路由切分在特定頁面只載入相關的腳本與樣式檔,譬如在個人資料頁面就不載入商品或是行銷活動頁面相關程式碼。
  2. 常用的第三方套件被放在某個 common.chunk.js 中,這樣瀏覽器就能夠進行快取,這樣即使要看到新版的應用,也不一定要重新下載這個部分。
  3. CSS-in-JS 的方案讓樣式檔可以跟著元件一起載入,或是盡量使用原子化設計的撰寫方式,並且把相關的樣式都放在 common.css 中讓瀏覽器進行快取。

會有 code splitting 另一方面也是 HTTP2 出現之後的影響,針對同一個網域可以平行處理資源,但值得注意的是經網路上的大大測試同時很多小檔也不一定比一個大檔案快,所以其實是要嘗試出一個中間值。

打包檔分析器 (Bundle Analyzer)

在使用的套件越來越多的情況底下,其實延遲載入、程式拆分的做法改善也有限,有時候其實很難發覺到底是哪個套件對專案影響最大,這時候就可以使用打包檔分析器來找看看兇手是誰,解決的方式也許是看看

  1. import 的時候使用 partial import 的方式而不是直接全部引入 (import has from 'lodash/has';
    vs import { has } from 'lodash';),這個部分跟 code splitting 其實是類似的概念
  2. 找替代方案,像是 moment.js 就屬於大包的套件,就有 day.js 或是 date-fns 這類相對小包的套件可作替代

範例頁面可以看這裡:
https://cdn.rawgit.com/danvk/source-map-explorer/08b0e130cb9345f9061760bf8a8d9136ea60b457/demo.html

資源優化 (Resource Optimization)

  1. 透過撰寫 serviceWorker 來實作 PWA 靜態資源快取
1
2
3
4
5
6
7
8
9
10
11
12
13
self.addEventListener("install", function (event) {
event.waitUntil(
caches.open(cacheName).then(function (cache) {
return cache.addAll([
"/css/bootstrap.css",
"/css/main.css",
"/js/bootstrap.min.js",
"/js/jquery.min.js",
"/offline.html",
]);
})
);
});
  1. 資料面的快取
  1. 靜態圖片資源進行無損壓縮
  1. Responsive Images

網站速度測試工具

Google 提供了底下兩套免費的工具 PageSpeed Insights 較適合非開發者使用會是用真實環境去測試,Chrome Lighthouse 則比較適合前端工程師本地端測試使用。

PageSpeed Insights
PageSpeed Insights

Chrome Lighthouse
Chrome Lighthouse


喜歡這篇文章,請幫忙拍拍手喔 🤣


share