Cách sử dụng webpack để làm cho ứng dụng nhỏ nhất có thể
Một trong những việc đầu tiên cần làm khi tối ưu hoá ứng dụng là làm cho ứng dụng càng nhỏ càng tốt. Sau đây là cách thực hiện việc này bằng gói web.
Sử dụng chế độ phát hành công khai (chỉ dành cho gói web 4)
Webpack 4 đã ra mắt cờ mode
mới. Bạn có thể đặt cờ này thành 'development'
hoặc 'production'
để gợi ý cho gói web rằng bạn đang xây dựng ứng dụng cho một môi trường cụ thể:
// webpack.config.js
module.exports = {
mode: 'production',
};
Đừng quên bật chế độ production
khi bạn xây dựng ứng dụng để phát hành công khai.
Điều này sẽ giúp gói web áp dụng các biện pháp tối ưu hoá như giảm thiểu, xoá mã chỉ dành cho nhà phát triển trong thư viện và nhiều tính năng khác.
Tài liệu đọc thêm
Bật tính năng giảm thiểu
Rút gọn là khi bạn nén mã bằng cách xoá các dấu cách thừa, rút ngắn tên biến, v.v. Chẳng hạn như:
// Original code
function map(array, iteratee) {
let index = -1;
const length = array == null ? 0 : array.length;
const result = new Array(length);
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
↓
// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}
Gói web hỗ trợ 2 cách để giảm thiểu mã: giảm kích thước ở cấp gói và các tuỳ chọn dành riêng cho trình tải. Bạn nên sử dụng đồng thời.
Rút gọn cấp gói
Phương pháp giảm thiểu cấp gói sẽ nén toàn bộ gói sau khi biên dịch. Dưới đây là cách hoạt động:
Bạn sẽ viết mã như sau:
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
Webpack biên dịch thành phần như sau:
// bundle.js (part of) "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["render"] = render; /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__); function render(data, target) { console.log('Rendered!'); }
Máy giảm thiểu nén dữ liệu thành những khoảng sau:
// minified bundle.js (part of) "use strict";function t(e,n){console.log("Rendered!")} Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
Trong gói web 4, tính năng giảm thiểu cấp gói sẽ tự động được bật – cả ở chế độ phát hành chính thức và khi chưa có. Công cụ này sử dụng trình rút gọn UglifyJS. (Nếu cần tắt tính năng giảm thiểu, bạn chỉ cần sử dụng chế độ phát triển hoặc chuyển false
vào tuỳ chọn optimization.minimize
.)
Trong webpack 3, bạn cần sử dụng trực tiếp trình bổ trợ UglifyJS. Trình bổ trợ này đi kèm với gói web; để bật trình bổ trợ này, hãy thêm trình bổ trợ này vào phần plugins
của cấu hình:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
Tuỳ chọn dành riêng cho trình tải
Cách thứ hai để rút gọn mã là các tuỳ chọn dành riêng cho trình tải (trình tải là gì). Với các tuỳ chọn trình tải, bạn có thể nén những thứ mà trình rút gọn không thể giảm kích thước. Ví dụ: khi bạn nhập một tệp CSS bằng css-loader
, tệp này sẽ được biên dịch thành một chuỗi:
/* comments.css */
.comment {
color: black;
}
// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n color: black;\r\n}",""]);
Trình rút gọn không thể nén mã này vì đây là một chuỗi. Để giảm kích thước nội dung tệp, chúng ta cần định cấu hình trình tải để thực hiện việc này:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
Tài liệu đọc thêm
- Tài liệu về UglifyJsPlugin
- Các trình thu nhỏ phổ biến khác: Babel Minify, Trình biên dịch đóng của Google
Chỉ định NODE_ENV=production
Một cách khác để giảm kích thước giao diện người dùng là đặt NODE_ENV
biến môi trường trong mã của bạn thành giá trị production
.
Các thư viện sẽ đọc biến NODE_ENV
để phát hiện xem chúng sẽ hoạt động ở chế độ nào – trong phiên bản phát triển hoặc phiên bản chính thức. Một số thư viện hoạt động theo cách khác dựa trên biến này. Ví dụ: khi NODE_ENV
không được đặt thành production
, Vue.js sẽ thực hiện thêm các bước kiểm tra và in cảnh báo:
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …
React hoạt động tương tự – nó tải một bản dựng đang phát triển bao gồm các cảnh báo:
// react/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
// react/cjs/react.development.js
// …
warning$3(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
// …
Các bước kiểm tra và cảnh báo như vậy thường không cần thiết trong phiên bản chính thức, nhưng vẫn tồn tại trong mã và tăng kích thước thư viện. Trong gói web 4, hãy xoá chúng bằng cách thêm tuỳ chọn optimization.nodeEnv: 'production'
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
nodeEnv: 'production',
minimize: true,
},
};
Trong gói web 3, hãy sử dụng DefinePlugin
:
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.optimize.UglifyJsPlugin()
]
};
Cả tuỳ chọn optimization.nodeEnv
và DefinePlugin
đều hoạt động theo cùng một cách – chúng thay thế mọi lần xuất hiện của process.env.NODE_ENV
bằng giá trị được chỉ định. Với cấu hình ở trên:
Webpack sẽ thay thế tất cả các lần xuất hiện của
process.env.NODE_ENV
bằng"production"
:// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if (process.env.NODE_ENV !== 'production') { warn('props must be strings when using array syntax.'); }
↓
// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
Sau đó, trình rút gọn sẽ xoá tất cả nhánh
if
như vậy – vì"production" !== 'production'
luôn sai và trình bổ trợ hiểu rằng mã bên trong các nhánh này sẽ không bao giờ thực thi:// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
↓
// vue/dist/vue.runtime.esm.js (without minification) if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; }
Tài liệu đọc thêm
- "Biến môi trường" là gì
- Tài liệu về gói web về:
DefinePlugin
,EnvironmentPlugin
Sử dụng mô-đun ES
Cách tiếp theo để giảm kích thước giao diện người dùng là dùng các mô-đun ES.
Khi bạn sử dụng các mô-đun ES, gói web có thể thực hiện việc lắc cây. Lắc cây là khi một trình đóng gói đi qua toàn bộ cây phần phụ thuộc, kiểm tra xem phần phụ thuộc nào được sử dụng và loại bỏ những phần phụ thuộc không dùng đến. Vì vậy, nếu bạn sử dụng cú pháp mô-đun ES, webpack có thể loại bỏ mã không dùng đến:
Bạn viết một tệp có nhiều tệp xuất, nhưng ứng dụng chỉ sử dụng một trong các tệp đó:
// comments.js export const render = () => { return 'Rendered!'; }; export const commentRestEndpoint = '/rest/comments'; // index.js import { render } from './comments.js'; render();
Webpack hiểu được rằng
commentRestEndpoint
không được sử dụng và không tạo ra điểm xuất riêng trong gói:// bundle.js (part that corresponds to comments.js) (function(module, __webpack_exports__, __webpack_require__) { "use strict"; const render = () => { return 'Rendered!'; }; /* harmony export (immutable) */ __webpack_exports__["a"] = render; const commentRestEndpoint = '/rest/comments'; /* unused harmony export commentRestEndpoint */ })
Trình rút gọn sẽ xoá biến không dùng đến:
// bundle.js (part that corresponds to comments.js) (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
Tính năng này hoạt động ngay cả với các thư viện nếu được viết bằng mô-đun ES.
Tuy nhiên, bạn không cần phải sử dụng chính xác trình rút gọn tích hợp sẵn (UglifyJsPlugin
) của gói web.
Bất kỳ trình rút gọn nào hỗ trợ việc loại bỏ mã bị chết (ví dụ: trình bổ trợ Babel Minify hoặc trình bổ trợ Trình biên dịch đóng của Google) đều có thể xử lý được.
Tài liệu đọc thêm
Tài liệu webpack về cây rung lắc
Tối ưu hóa hình ảnh
Hình ảnh chiếm hơn một nửa kích thước trang. Mặc dù không quan trọng như JavaScript (ví dụ: chúng không chặn hoạt động hiển thị), nhưng vẫn chiếm một phần lớn băng thông. Sử dụng url-loader
, svg-url-loader
và image-webpack-loader
để tối ưu hoá trong gói web.
url-loader
đặt cùng dòng các tệp tĩnh nhỏ vào ứng dụng. Nếu không có cấu hình, bộ mã hoá này sẽ lấy một tệp đã truyền, đặt tệp đó bên cạnh gói đã biên dịch và trả về URL của tệp đó. Tuy nhiên, nếu chúng tôi chỉ định tuỳ chọn limit
, thì tuỳ chọn này sẽ mã hoá các tệp nhỏ hơn giới hạn này dưới dạng một URL dữ liệu Base64 và trả về URL này. Thao tác này sẽ chèn hình ảnh vào mã JavaScript và lưu yêu cầu HTTP:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
},
},
],
}
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`
svg-url-loader
hoạt động giống như url-loader
– ngoại trừ việc mã này mã hoá các tệp bằng mã hoá URL thay vì mã Base64. Điều này hữu ích cho hình ảnh SVG – vì tệp SVG chỉ là một văn bản thuần tuý nên phương thức mã hoá này giúp tiết kiệm kích thước hơn.
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: "svg-url-loader",
options: {
limit: 10 * 1024,
noquotes: true
}
}
]
}
};
image-webpack-loader
nén các hình ảnh đi qua nó. Giao diện này hỗ trợ hình ảnh JPG, PNG, GIF và SVG, vì vậy chúng ta sẽ sử dụng định dạng này cho tất cả các loại này.
Trình tải này không nhúng hình ảnh vào ứng dụng, vì vậy, trình tải này phải hoạt động kết hợp với url-loader
và svg-url-loader
. Để tránh việc sao chép và dán tệp này vào cả hai quy tắc (một quy tắc cho hình ảnh JPG/PNG/GIF và một quy tắc khác cho các hình ảnh SVG), chúng tôi sẽ đưa trình tải này vào dưới dạng một quy tắc riêng với enforce: 'pre'
:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// This will apply the loader before the other ones
enforce: 'pre'
}
]
}
};
Chế độ cài đặt mặc định của trình tải đã sẵn sàng hoạt động – nhưng nếu bạn muốn định cấu hình hơn nữa, hãy xem các tuỳ chọn trình bổ trợ. Để chọn tuỳ chọn cần chỉ định, hãy xem hướng dẫn rất hay về cách tối ưu hoá hình ảnh của Addy Osmani.
Tài liệu đọc thêm
- "Mã hoá base64 được dùng để làm gì?"
- Hướng dẫn tối ưu hoá hình ảnh của Addy Osmani
Tối ưu hoá phần phụ thuộc
Hơn một nửa kích thước JavaScript trung bình đến từ các phần phụ thuộc và một phần kích thước đó có thể là không cần thiết.
Ví dụ: Lodash (kể từ phiên bản 4.17.4) thêm 72 KB mã rút gọn vào gói. Nhưng nếu bạn chỉ sử dụng 20 phương thức, thì khoảng 65 KB mã rút gọn sẽ chẳng có tác dụng gì.
Một ví dụ khác là Moment.js. Phiên bản 2.19.1 của phiên bản này chiếm 223 KB mã rút gọn, một con số rất lớn – kích thước trung bình của JavaScript trên một trang là 452 KB vào tháng 10 năm 2017. Tuy nhiên, 170 KB của kích thước đó là tệp bản địa hoá. Nếu bạn không sử dụng Moment.js cho nhiều ngôn ngữ, các tệp này sẽ làm tăng gói mà không có mục đích.
Bạn có thể dễ dàng tối ưu hoá tất cả các phần phụ thuộc này. Chúng tôi đã thu thập các phương pháp tối ưu hoá trong kho lưu trữ GitHub – hãy xem!
Bật tính năng nối mô-đun cho các mô-đun ES (còn gọi là chuyển phạm vi lên trên)
Khi bạn tạo một gói, gói web sẽ gói từng mô-đun vào một hàm:
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓
// bundle.js (part of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_exports__["a"] = render;
function render(data, target) {
console.log('Rendered!');
}
})
Trước đây, đây là yêu cầu để tách biệt các mô-đun CommonJS/AMD với nhau. Tuy nhiên, việc này đã làm tăng thêm mức hao tổn kích thước và hiệu suất cho từng mô-đun.
Webpack 2 giới thiệu tính năng hỗ trợ cho các mô-đun ES, không giống như các mô-đun CommonJS và AMD, có thể đóng gói mà không cần gói từng mô-đun bằng một hàm. Và webpack 3 đã giúp việc gói như vậy có thể được thực hiện – với nối mô-đun. Sau đây là chức năng của việc nối mô-đun:
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓
// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files
// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// CONCATENATED MODULE: ./comments.js
function render(data, target) {
console.log('Rendered!');
}
// CONCATENATED MODULE: ./index.js
render();
})
Bạn thấy sự khác biệt? Trong gói thuần tuý, mô-đun 0 yêu cầu render
từ mô-đun 1. Với việc nối mô-đun, require
chỉ được thay thế bằng hàm bắt buộc và mô-đun 1 sẽ bị xoá. Gói có ít mô-đun hơn – và ít hao tổn mô-đun hơn!
Để bật hành vi này, trong gói web 4, hãy bật tuỳ chọn optimization.concatenateModules
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true
}
};
Trong gói web 3, hãy sử dụng ModuleConcatenationPlugin
:
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
Tài liệu đọc thêm
- Tài liệu về gói web cho ModuleConcatenationPlugin
- "Giới thiệu ngắn gọn về việc di chuyển phạm vi lên trên"
- Mô tả chi tiết về chức năng của trình bổ trợ này
Sử dụng externals
nếu bạn có cả mã gói web và mã không phải gói web
Có thể bạn có một dự án lớn, trong đó có một số mã được biên dịch bằng webpack, còn một số mã thì không. Giống như trang web lưu trữ video, trong đó tiện ích trình phát có thể được xây dựng bằng gói web và trang xung quanh có thể không:
Nếu cả hai đoạn mã có các phần phụ thuộc chung, thì bạn có thể chia sẻ các phần phụ thuộc đó để tránh tải mã xuống nhiều lần. Bạn có thể thực hiện việc này bằng tuỳ chọn externals
của gói web – nó thay thế các mô-đun bằng các biến hoặc các dữ liệu nhập bên ngoài khác.
Liệu các phần phụ thuộc có sẵn trong window
hay không
Nếu mã không phải gói web của bạn dựa vào các phần phụ thuộc có sẵn dưới dạng biến trong window
, thì tên phần phụ thuộc đại diện cho tên biến:
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
Với cấu hình này, gói web sẽ không nhóm các gói react
và react-dom
. Thay vào đó, các thành phần này sẽ được thay thế bằng mã như sau:
// bundle.js (part of)
(function(module, exports) {
// A module that exports `window.React`. Without `externals`,
// this module would include the whole React bundle
module.exports = React;
}),
(function(module, exports) {
// A module that exports `window.ReactDOM`. Without `externals`,
// this module would include the whole ReactDOM bundle
module.exports = ReactDOM;
})
Nếu các phần phụ thuộc được tải dưới dạng gói AMD
Nếu mã không phải gói web của bạn không hiển thị các phần phụ thuộc vào window
thì mọi thứ sẽ phức tạp hơn.
Tuy nhiên, bạn vẫn có thể tránh tải cùng một mã hai lần nếu mã không phải gói web sử dụng các phần phụ thuộc này dưới dạng gói AMD.
Để thực hiện việc này, hãy biên dịch mã webpack dưới dạng gói AMD và mô-đun đại diện thành các URL thư viện:
// webpack.config.js
module.exports = {
output: {
libraryTarget: 'amd'
},
externals: {
'react': {
amd: '/libraries/react.min.js'
},
'react-dom': {
amd: '/libraries/react-dom.min.js'
}
}
};
Webpack sẽ gói gói vào define()
và làm cho gói phụ thuộc vào các URL sau:
// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
Nếu mã không phải gói web sử dụng cùng các URL để tải các phần phụ thuộc, thì các tệp này sẽ chỉ được tải một lần – các yêu cầu bổ sung sẽ sử dụng bộ nhớ đệm của trình tải.
Tài liệu đọc thêm
- Tài liệu về gói web trên
externals
Tổng hợp
- Bật chế độ phát hành công khai nếu bạn sử dụng webpack 4
- Giảm thiểu mã bằng các tuỳ chọn trình tải và trình thu thập ở cấp gói
- Xoá mã chỉ dành cho hoạt động phát triển bằng cách thay thế
NODE_ENV
bằngproduction
- Sử dụng các mô-đun ES để bật tính năng rung cây
- Nén hình ảnh
- Áp dụng các biện pháp tối ưu hoá dành riêng cho phần phụ thuộc
- Bật tính năng nối mô-đun
- Sử dụng
externals
nếu thấy phù hợp với bạn