Web優化訓練營, 網頁提速50倍
前言
我們將通過一個完整的實例, 一步步的優化加載, 渲染等各方面的體驗.
開始
首先我們先看一下項目的文件構成
這之中包含了一個基本網頁的元素, js(React App), css, 還有圖片.
我們先來看一下來serve整個網頁的部分.
server.js
'use strict';
const fs = require('fs');
const path = require('path');
const koa = require('koa');
const app = koa();
app.use(function* (next) {
const file = this.path.slice(1) || 'index.html';
try {
const content = yield cb => fs.readFile(path.resolve('./dist', file), cb);
this.body = content;
this.type = path.extname(file).slice(1);
this.status = 200;
} catch (e) {
this.status = 404;
}
yield next;
});
app.listen(process.env.PORT || 3000);</code></pre>
這段代碼只是簡單的將 dist 目錄下的文件給轉發一下.
打開網頁便可以看到相關加載情況.

我們可以看到, 整個 app.js 共277kb, 在模擬3G網絡的情況下(藍色框框),每次加載需要花費999ms, 其中下載花費了911ms(紅色框框).
接下來我們將逐步優化, 然后每次將結果進行比較.
優化(一) --- 304
網頁加載優化中最常見的就是 304 Not Modified 了, 具體機制是瀏覽器發起請求, headers中包含 If-Modified-Since ,(如無緩存, 則無此頭字段), 服務器對比硬盤上(或內存中)文件最后修改的時間, 如果小于或等于請求的時間, 則返回304. 否則, 則返回200, 并加上 Last-Modified 字段, 告訴客戶端下次請求可以嘗試請求是否有緩存.
具體代碼如下:
app.use(function* () {
const file = path.resolve(__dirname, path.resolve('dist', this.path.slice(1) || 'index.html'));
const headers = this.headers;
let ifLastModified = this.headers['if-modified-since'];
if (ifLastModified) {
ifLastModified = new Date(ifLastModified);
}
try {
const stat = yield cb => fs.stat(file, cb);
const now = Date.now();
if (ifLastModified &&
file !== path.resolve(__dirname, path.resolve('dist/index.html'))) {
if (ifLastModified >= stat.mtime) {
this.status = 304;
return;
}
}
console.log(file)
const content = yield cb => fs.readFile(file, cb);
this.body = content;
this.type = path.extname(file).slice(1);
this.status = 200;
this.set('Last-Modified', stat.mtime);
} catch (e) {
this.status = 404;
}
});</code></pre>
(模擬實際情況中, 首頁會動態生成, 加入一些廣告,追蹤或個性化數據, index.html 并未緩存)
最終效果:

我們可以看見, 下載時間為2ms, 可以幾乎忽略掉(只有HTTP Headers), 總共的加載時間也只有了120ms, 相比之前, 整整少了 869ms.
但是, 我們滿足了嗎?
優化(二) --- 分別打包
我們可以注意到, 我們打包出來最終只有一個js文件, 當依賴變多后(此例中只有react和react-dom, 每次修改都導致整個js文件被重新請求.所以我們想要把不同的library(甚至是項目內部公用的代碼模塊)提取出來.
我們首先要創建一個 webpack.vendors.config.js 來構建這些library, 或者vendor.
const path = require('path');
const WebpackCleanupPlugin = require('webpack-cleanup-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"',
},
}),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true,
drop_console: true,
drop_debugger: true,
},
}),
new webpack.DllPlugin({
path: path.resolve(dirname, 'dist/vendor/[name]-manifest.json'),
name: '[name]',
context: '.',
}),
],
devtool: 'hidden-source-map',
entry: {
'react': ['react', 'react-dom'],
},
output: {
path: path.resolve(dirname, 'dist/vendor'),
filename: '[name].js',
library: '[name]',
},
};</code></pre>
注意到
entry: {
'react': ['react', 'react-dom'],
},
意味著我們可以將同一類型的包打包成一個js文件.
當然, 我們也要對 webpack.production.js 做一些修改.
const dlls = fs.readdirSync(path.resolve(__dirname, 'dist/vendor/'))
.filter(file => path.extname(file) === '.js')
.map(file => path.basename(file, '.js'))
const dllReferencePlugins = dlls
.map(dll =>
new webpack.DllReferencePlugin({
context: '.',
manifest: require(./dist/vendor/${dll}-manifest.json
),
})
);
module.exports = {
plugins: dllReferencePlugins.concat([
...
]),
...
}</code></pre>
在這, 我們將自動掃描 vendor 目錄下面的文件, 自動將所有的vendor加載進來.
這樣我們就實現了分包加載(還有一些細節的修改, 包括 index.html , 請參見github上, step-2 分支)

效果還是不錯的, app.js 單獨加載只需要400多ms, 比起所有依賴一起加載要快了至少一半以上.
對于一般類型網站, 優化到這已經可以取得非常不錯的效果了, 但是對于大型網站來說, 我們可以做的還有很多.
優化(三) --- 強制緩存
我們可以注意到優化一種, 一個304的請求仍然花掉了100多毫秒, 對于大型網站, 資源特別多的情況, 這仍然是一個不小的開支. 那我們可以把這個省掉嗎? 答案是可以的.
瀏覽器緩存當中, 還有一個特別的字段. Expires , 它可以指定文件的過期時間, 直到那一刻位置, 瀏覽器都不會再重新發起請求, 而是直接從本地緩存中讀取.
但是, 這仍舊需要每隔一段時間去請求. 我們該如何做呢? 答案就是, 設置超長的緩存時間, 例如10年. 但是這樣我們便無法更新任何內容了. 我們該如何用到這樣的特性, 而又很方便的更新呢.
我們可以給文件名加上 hash特征值 , 這樣只有當文件內容有改動時, 才會重新加載, 而且這樣適合于分布式CDN的, 非覆蓋式的發布, 可以使其在引用頁面(首頁)已經改變的情況下(當前服務器已經發布), 才會用到新資源, 而訪問到未發布的服務器時, 還是會引用老的資源, 使得發布再也不需要熬夜
具體細節改動見git branch step-3.
實現效果:

可以從藍色方框出看見, 緩存已經生效, 而整體的讀取時間才只有20毫秒不到.
從原始的1000毫秒, 到現在的20毫秒, 簡簡單單的三個步驟便可以讓你的網頁加載提速50倍
擴展閱讀
1.在實際生產中, 我們通常看到的是加載的CDN域名, 這是為何呢?

這是因為, 一個大型的網站, 請求當中會帶上很多Cookie, 有的甚至于接近1KB, 而100個圖片的加載, 就是整整100KB. 通過第三方域名(不同于當前域名), 我們可以節省掉許多不必要的請求頭, Cookie頭. 同樣達到提速的目的
2.還有一種情況是, 資源分布在不同的服務器上

這是因為瀏覽器對于同一域名下資源的并行下載數量有限制.

使用不同的資源服務器可以避開這種限制, 加大下載并發數. 但是, 這樣同樣帶來的緩存命中率的問題, 所以還需要存儲用戶緩存相關的數據. 合理的利用下, 對于頁面整體的加載速度還是很有好處的.
3.其他的方法
在技術飛速發展的當下, 還有很多技術都是可以對終端用戶的體驗帶來提升的.
- BigPipe + Server-Side Rendering, 加速首頁加載速度
- Goole AMP
- HTTP/2
來自:http://tech.dianwoda.com/2016/11/01/web-load-optimization-step-by-step/