Cách sử dụng webpack để tạo ứ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à tạo ứng dụng nhỏ bằng nhất có thể. Sau đây là cách thực hiện việc này bằng webpack.
Sử dụng chế độ sản xuất (chỉ dành cho webpack 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 ý gói web mà 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',
};
Hãy nhớ bật chế độ production
khi bạn xây dựng ứng dụng để phát hành công khai.
Thao tác này sẽ giúp gói web áp dụng các biện pháp tối ưu hoá như giảm kích thước, xoá mã chỉ dành cho nhà phát triển
trong thư viện và nhiều nội dung khác.
Tài liệu đọc thêm
Bật tính năng thu nhỏ
Giảm thiểu là khi bạn nén mã bằng cách xoá khoảng trắng thừa, rút ngắn tên biến và 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}
Webpack hỗ trợ hai cách để giảm kích thước mã: giảm thiểu ở cấp gói và tuỳ chọn dành riêng cho trình tải. Bạn nên sử dụng đồng thời các tính năng này.
Giảm kích thước gói ở cấp độ gói
Việc rút gọn 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 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 khoảng 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!'); }
Trình rút gọn nén thông tin này thành những thành phần gần như 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 webpack 4, tính năng thu nhỏ cấp gói được bật tự động – cả trong phiên bản chính thức
và không có. Công cụ này sử dụng trình rút gọn UglifyJS
nâng cao. (Nếu cần tắt tính năng rút gọn, 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ình bổ trợ UglifyJS
trực tiếp. Trình bổ trợ này đi kèm với gói web; để bật, hãy thêm vào 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 để giảm kích thước 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ông tin
không thể giảm kích thước. Ví dụ: khi bạn nhập tệp CSS có
css-loader
, tệp đượ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 Minifi phổ biến khác: Babel Giảm kích thước, Đóng của Google Trình biên dịch
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ã thành giá trị production
.
Các thư viện sẽ đọc biến NODE_ENV
để xác định xem chúng nên hoạt động ở chế độ nào – trong phần tử
phát triển hoặc sản xuất. Một số thư viện hoạt động theo cách khác dựa trên biến này. Cho
ví dụ: khi không đặt NODE_ENV
thành production
, Vue.js sẽ thực hiện các bước kiểm tra và in bổ sung
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ự – ứng dụng này tải một bản dựng phát triển có 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 chúng vẫn tồn tại trong mã và
hãy tăng kích thước thư viện. Trong webpack 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 webpack 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 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ững
if
nhánh – vì"production" !== 'production'
luôn luôn false, 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ề:
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à sử dụng ES các mô-đun.
Khi bạn sử dụng các mô-đun ES, gói webpack có thể thực hiện kỹ thuật rung cây. Rắc cây là khi người đi bộ truyền tải toàn bộ cây phần phụ thuộc, kiểm tra xem phần phụ thuộc nào được dùng và xoá 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 số 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 rằng
commentRestEndpoint
không được sử dụng và không tạo ra điểm xuất riêng biệt 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ác thư viện đó được viết bằng các mô-đun ES.
Tuy nhiên, bạn không bắt buộc phải sử dụng chính xác trình thu nhỏ tích hợp sẵn của webpack (UglifyJsPlugin
).
Bất kỳ trình khai thác nào hỗ trợ tính năng xoá mã lỗi
(ví dụ: trình bổ trợ Babel Minify
hoặc trình bổ trợ Trình biên dịch đóng của Google)
sẽ giải quyết vấn đề.
Tài liệu đọc thêm
Tài liệu trên webpack về việc rung cây
Tối ưu hóa hình ảnh
Hình ảnh chiếm hơn
một nửa kích thước trang. Trong khi
không quan trọng như JavaScript (ví dụ: chúng không chặn hiển thị), chúng vẫn chiếm phần lớn
băng thông. Sử dụng url-loader
, svg-url-loader
và image-webpack-loader
để tối ưu hoá chúng trong
gói web.
url-loader
đưa các tệp tĩnh nhỏ vào cùng dòng
. Nếu không có cấu hình, hệ thống sẽ lấy một tệp đã truyền, đặt tệp đó bên cạnh gói được biên dịch và trả về
url của tệp đó. Tuy nhiên, nếu chúng ta chỉ định tuỳ chọn limit
, 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. Chiến dịch này
nội dòng 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 có URL
mã hoá thay vì Base64
một. Phương thức này hữu ích cho hình ảnh SVG – vì tệp SVG chỉ là văn bản thuần tuý nên phương thức mã hoá này
hiệu quả hơn về kích thước.
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: "svg-url-loader",
options: {
limit: 10 * 1024,
noquotes: true
}
}
]
}
};
image-webpack-loader
nén hình ảnh tải lên
thông qua đó. Định dạng 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 định dạng này.
Trình tải này không nhúng hình ảnh vào ứng dụng nên phải hoạt động cùng với url-loader
và
svg-url-loader
Để tránh sao chép-dán hình ảnh vào cả hai quy tắc (một cho hình ảnh JPG/PNG/GIF và một quy tắc khác
một cho định dạng SVG), chúng tôi sẽ thêm trình tải này 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'
}
]
}
};
Các 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 nó hãy xem các tuỳ chọn trình bổ trợ. Người nhận chọn những tùy chọn để chỉ định, hãy xem hướng dẫn về hình ảnh tuyệt vời của Addy Osmani tối ưu hoá.
Tài liệu đọc thêm
- "Định nghĩa về phương thức mã hoá base64 cho?"
- Hướng dẫn của Addy Osmani về cách tối ưu hoá hình ảnh
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 của 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, như 20 phương thức của nó, sau đó khoảng 65 KB mã rút gọn không làm gì cả.
Một ví dụ khác là Moment.js. Phiên bản 2.19.1 của nó mất 223 KB mã rút gọn, 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 ở kích thước đó là bản địa hoá tệp. Nếu bạn không sử dụng Moment.js với nhiều ngôn ngữ, những tệp này sẽ làm tăng kích thước của 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 một kho lưu trữ GitHub – hãy xem thử!
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 webpack 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, bạn phải làm điều này để tách riêng các mô-đun CommonJS/AMD với nhau. Tuy nhiên, điều này đã làm tăng mức hao tổn kích thước và hiệu suất cho mỗi mô-đun.
Webpack 2 ra mắt tính năng hỗ trợ các mô-đun ES, không giống như các mô-đun CommonJS và AMD, có thể đi kèm mà không cần gói từng hàm bằng một hàm. Và webpack 3 đã thực hiện được việc gói như vậy – với nối mô-đun. Sau đây là thì việc nối mô-đun có chức năng gì:
// 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 có thấy sự khác biệt? Trong gói thuần tuý, mô-đun 0 cần có render
từ mô-đun 1. Bằng
việc nối mô-đun, require
chỉ được thay thế bằng hàm bắt buộc và mô-đun 1 là
đã bị xóa. Gói này có ít mô-đun hơn và mức hao tổn mô-đun ít hơn!
Để bật hành vi này, trong webpack 4, hãy bật tuỳ chọn optimization.concatenateModules
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true
}
};
Trong webpack 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 dành cho ModuleConcatenationPlugin
- "Giới thiệu ngắn gọn về phạm vi nâng cấp"
- Mô tả chi tiết về những gì trình bổ trợ này
Sử dụng externals
nếu bạn có cả mã webpack và mã không phải webpack
Bạn có thể sở hữu một dự án lớn, trong đó một số mã được biên dịch bằng webpack, còn một số mã thì không. Thích trang web lưu trữ video, nơi tiện ích trình phát có thể được xây dựng bằng webpack và trang xung quanh có thể không phải là:
Nếu cả hai đoạn mã đều có phần phụ thuộc chung, thì bạn có thể chia sẻ chúng để tránh tải mã xuống
nhiều lần. Việc này được thực hiện bằng externals
của gói webpack
tuỳ chọ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.
Nếu phần phụ thuộc có trong window
Nếu mã không phải webpack 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
, hãy đại diện
tên phần phụ thuộc thành 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 đó, chúng sẽ
được thay thế bằng nội dung tương tự:
// 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 webpack 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 những
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 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 webpack sử dụng cùng URL để tải các phần phụ thuộc thì các tệp này sẽ đượ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 Webpack trên
externals
Tổng hợp
- Bật chế độ phát hành chính thức nếu bạn sử dụng webpack 4
- Giảm thiểu mã của bạn bằng các tuỳ chọn trình thu gọn và trình tải ở cấp gói
- Xoá mã chỉ dành cho nhà 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 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
- Hãy sử dụng
externals
nếu lựa chọn này phù hợp với bạn