Do hoàn cảnh xô đẩy nên tôi ngoài việc “Trên thông A.I, dưới thạo M.L”, phải đá sang MEARN stack. Sau một thời gian dùng webpack để build thì song song với việc nhàn hạ thì tôi đã gặp ác mộng khi dung lượng của file bundle càng ngày càng phình ra ảnh hưởng lớn tới tốc độ tải trang web. Bài này tập trung về giải pháp tối ưu hoá dung lượng bundle.
1. Giới thiệu nhanh về webpack
Hiện tại các website đang tiến tới xu hướng trở thành những web app với các đặc tính sau:
- Sử dụng Javascrip nhiều hơn
- Hạn chế phát triển full-page reload, tập trung single-page app.
- Webbrower hỗ trợ được nhiều công nghệ mới
Vì vậy webpack là 1 công cụ hữu hiệu để quản số lượng source code khổng lồ phía client-side.
Do mục đích của bài viết chỉ tập trung về việc tối ưu hóa dung lượng của bundle được webpack tạo ra, nên tôi xin phép sẽ không đi sâu vào việc giới thiệu webpack ở đây.
2. Vấn đề phát sinh
Khó có thể chấp nhận được 1 file bundle.js có dung lượng 8 MB (chưa minify) hoặc 5 MB (đã minify) dẫn tới việc tải file này mất ~4 phút từ server Mỹ, dẫn đến thời gian người dùng chờ tải trang web thành 1 cực hình.
Dưới đây là quá trình build file từ webpack.
File bundle.js nặng 8.58 MB khi chưa thực hiện minify
Dung lượng giảm xuống còn 5.08 MB khi thực hiện minify:
Sau quá trình trên tôi đã tạo ra 1 file bundle.js duy nhất, chứa toàn bộ source code của phía frontend (trừ các file ảnh).
Mổ xẻ nội dung file bundle.js này sẽ giúp tôi phán đoán được dung lượng các thành phần cấu tạo nên file:
Có 3 thành phần chiếm phần lớn dung lượng của file bundle.js này
- font chữ
- node_module
- source code dự án
Hừm, việc nhét tất cả resouce vào 1 file được cái lợi nhàn hạ trước mắt cuối cùng cũng đã biến thành cơn ác mộng của những người làm lập trình chân chính
Ok, ngừng kêu la, gạt nước mũi và bắt tay vào việc giải quyết vấn đề
Nội dung file webpack.config.js hiện tại như sau:
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 42 43 44 45 46 | var webpack = require("webpack"); var autoprefixer = require('autoprefixer'); var precss = require('precss'); const fontsFileName = 'src/styles/fonts/[name].[ext]'; var path = require("path"); module.exports = { entry: [ './src/App.js' ], output: { path: __dirname, filename: './public/bundle.js' }, resolve: { alias: { api_v1: path.resolve(__dirname, 'src/module.exports/api_v1.js') }, extensions: ['.js', '.jsx'] }, module: { loaders: [ { loader: 'babel-loader', query: { presets: ['es2015', 'react', 'stage-0'] }, test: /\.jsx?$/, exclude: /node_modules/ }, { test: /\.scss$/, loader: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.less$/, loader: ['style-loader', 'css-loader', 'less-loader'] }, { test: /\.(eot|svg|ttf|woff|woff2|otf)$/, loader: 'url-loader?name=src/styles/fonts/[name].[ext]' } ] } } |
- Chỉ sử dụng 1 entry JS duy nhất
| entry: [ './src/App.js' ], |
- Đưa toàn bộ các file css, font chữ vào bundle
| { test: /\.scss$/, loader: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.less$/, loader: ['style-loader', 'css-loader', 'less-loader'] }, { test: /\.(eot|svg|ttf|woff|woff2|otf)$/, loader: 'url-loader?name=src/styles/fonts/[name].[ext]' } |
- Tất cả source code được gộp chung vào 1 file duy nhất bundle.js
| output: { path: __dirname, filename: './public/bundle.js' }, |
Ok, việc này khá tương đồng với 3 thành phần chiếm phần lớn dung lượng file bundle.js như đã phân tích bên trên.
Lọ mọ tra cứu trên mạng, tôi tổng hợp được rất nhiều phương án của con nhà người ta, tuy nhiên dưới đây là cách tôi đã áp dụng:
- Phân chia nhỏ entry point cho những page riêng biệt.
- Đưa toàn bộ những library dùng chung vào 1 file (vendor.js), file này sẽ được cache lại nên khi load những trang khác sẽ nhanh hơn.
Tuy nhiên bản chất của phương pháp trên chỉ là chia để trị, thay vì 1 file code to thì tôi chia ra nhiều file nhỏ và tải theo từng page cho phù hợp, tổng lại thì bundle size cũng không khác như cũ là bao nhiêu. Nhưng tốc độ sẽ được cải thiện rất nhiều do các file nhỏ sẽ được tải song song.
Sau rất nhiều lần thử sai , file webpack.config.js của tôi cuối cùng như sau:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | var webpack = require("webpack"); var autoprefixer = require('autoprefixer'); var precss = require('precss'); const fontsFileName = 'src/styles/fonts/[name].[ext]'; var path = require("path"); const CompressionPlugin = require("compression-webpack-plugin") module.exports = { plugins: [ // new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), // new webpack.optimize.AggressiveMergingPlugin({ // minSizeReduce: 2, // moveToParents: true, // }), // new webpack.optimize.AggressiveSplittingPlugin({ // minSize: 3000, //Byte, split point. Default: 30720 // maxSize: 5000, //Byte, maxsize of per file. Default: 51200 // chunkOverhead: 0, //Default: 0 // entryChunkMultiplicator: 1, //Default: 1 // }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, // Suppress uglification warnings pure_getters: true, unsafe: true, unsafe_comps: true, screw_ie8: true }, comments: false, sourceMap: false, exclude: [/\.min\.js$/gi] // skip pre-minified libs }), // new webpack.IgnorePlugin(/^\.\/locale$/, [/moment$/]), // Bug: TypeError: this.contextRegExp.test is not a function new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), // Avoid publishing files when compilation failed new webpack.NoEmitOnErrorsPlugin(), new CompressionPlugin({ asset: "[path].gz[query]", algorithm: "gzip", test: /\.js$|\.css$|\.html$/, threshold: 10240, minRatio: 0 }), // The CommonsChunkPlugin selects only entry chunks. // After the CCP processed the modules it creates a new entry chunk (the commons chunk) and make the used chunks non-entries. // When using multiple CCP they only extract modules from the last CCP. new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', // minChunks: (module) => /node_modules/.test(module.context) minChunks(module, count) { var context = module.context; return context && context.indexOf('node_modules') >= 0; }, }), new webpack.optimize.CommonsChunkPlugin({ name: 'react', minChunks(module, count) { var context = module.context; return context && context.indexOf('node_modules\/react') >= 0; }, }), ], // devtool: 'cheap-module-source-map', entry: { app: './src/App.js', }, output: { path: path.resolve(__dirname, 'public'), filename: '[name].bundle.js', chunkFilename: '[name].chunk.js', }, resolve: { alias: { api_v1: path.resolve(__dirname, 'src/module.exports/api_v1.js'), }, extensions: ['.js', '.jsx'], modules: [ path.resolve('./'), path.resolve('./node_modules'), ] }, module: { loaders: [ { loader: 'babel-loader', query: { presets: ['es2015', 'react', 'stage-0'] }, test: /\.jsx?$/, exclude: /node_modules/ }, { test: /\.scss$/, loader: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.less$/, loader: ['style-loader', 'css-loader', 'less-loader'] }, // { // test: /\.(eot|svg|ttf|woff|woff2|otf)$/, // loader: 'url-loader?name=src/styles/fonts/[name].[ext]' // } ] } } |
Trong đó có những cải tiến chính như sau
| new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), |
-> Chuyển môi trường build sang production, giúp loại bỏ bớt các thành phần thừa chỉ dùng để test trong quá trình phát triển
| new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, // Suppress uglification warnings pure_getters: true, unsafe: true, unsafe_comps: true, screw_ie8: true }, comments: false, sourceMap: false, exclude: [/\.min\.js$/gi] // skip pre-minified libs }), |
-> Thực hiện minify các file javascript, việc này tương đương với việc chạy câu lệnh build webpack -p
| new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), |
-> Tối ưu hóa việc sử dụng thư viện moment.js
| new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', // minChunks: (module) => /node_modules/.test(module.context) minChunks(module, count) { var context = module.context; return context && context.indexOf('node_modules') >= 0; }, }), new webpack.optimize.CommonsChunkPlugin({ name: 'react', minChunks(module, count) { var context = module.context; return context && context.indexOf('node_modules\/react') >= 0; }, }), |
=> Chia nhỏ webpack thành các file react.bundle.js chưa toàn bộ source code của thư viện react và vendor.bundle.js chứa toàn bộ thư viện còn lại
| // { // test: /\.(eot|svg|ttf|woff|woff2|otf)$/, // loader: 'url-loader?name=src/styles/fonts/[name].[ext]' // } |
-> Loại bỏ toàn bộ font chữ ra khỏi bundle.
Thử build lại webpack và tận hưởng thành quả nào
Wow, ít nhất thì màu xám nhàm chán của file bundle.js đã thành xanh xanh đỏ đỏ cho em nhỏ nó mừng!!!
Nhìn thì có vẻ đẹp, còn dung lượng của file thì sao?
Dung lượng các file nhỏ đã giảm đáng kể và thời gian tải về không quá 1.84 s
TUYỆT VỜI !!!!!
Có phải là đã xong việc chưa nhỉ?
Nếu tinh ý, các bạn có thể thấy tôi có comment đoạn code sau
| // new webpack.optimize.AggressiveSplittingPlugin({ // minSize: 3000, //Byte, split point. Default: 30720 // maxSize: 5000, //Byte, maxsize of per file. Default: 51200 // chunkOverhead: 0, //Default: 0 // entryChunkMultiplicator: 1, //Default: 1 // }), |
Mục đích của đoạn code này nhằm quy định dung lượng tối tiểu/ tối đa của 1 file khi file bundle sẽ được chia thành nhiều file khi CHỈ dựa vào kích thước đã được quy định sẵn.
Boom!, nếu tôi sử dụng đoạn code trên, file bundle của tôi sẽ thành thế này
Việc có nên chia nhỏ như thế này không, tôi sẽ để lại đôc giả tự trả lời nhé
3. Tóm lại
Tôi đã phân tích cách tôi giảm thời gian tải trang từ ~4 phút xuống còn 1.84 giây bằng cách tùy chỉnh lại file webpack.config.js.
Trên mạng có khá nhiều hướng dẫn với các cách tiếp cận khác nhau, tuy nhiên đối với dự án của tôi, thì cách trên là hiệu quả nhất
Các bạn có cách nào hiệu quả thì comment trao đổi nhé