Giảm kích thước giao diện người dùng

Cách sử dụng webpack để làm cho ứng dụng của bạn 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 nhỏ nhất có thể. Sau đây là cách thực hiện việc này bằng webpack.

Sử dụng chế độ phát hành chính thức (chỉ 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 ý cho webpack rằng bạn đang tạo ứ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 tạo ứng dụng để phát hành chính thức. Điều này sẽ giúp webpack áp dụng các tính năng tối ưu hoá như rút gọn, xoá mã chỉ dành cho 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 rút gọn

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}

Webpack hỗ trợ hai cách rút gọn mã: rút gọn ở cấp góicác tuỳ chọn dành riêng cho trình tải. Bạn nên sử dụng đồng thời cả hai.

Rút gọn ở 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:

  1. Bạn viết mã như sau:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack biên dịch tệp này thành nội dung gần giố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!');
    }
    
  3. Trình rút gọn sẽ nén mã này thành 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 rút gọn ở cấp gói sẽ tự động bật – cả ở chế độ phát hành và không có chế độ nào. Công cụ này sử dụng trình rút gọn UglifyJS trong phần 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 truyền false đến 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 webpack; để bật trình bổ trợ, hãy thêm trình bổ trợ đó 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(),
  ],
};

Các 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 nội dung mà trình rút gọn không thể rút gọn. Ví dụ: khi bạn nhập 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. Để rút gọn 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

Chỉ định NODE_ENV=production

Một cách khác để giảm kích thước phần giao diện người dùng là đặt biến môi trường NODE_ENV trong mã thành giá trị production.

Thư viện đọc biến NODE_ENV để phát hiện chế độ hoạt động của thư viện – trong quá trình phát triển hay trong quá trình sản xuất. Một số thư viện hoạt động theo cách khác nhau 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 các bước kiểm tra bổ sung 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ự – 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 quá trình phát hành chính thức, nhưng vẫn tồn tại trong mã và làm tăng kích thước thư viện. Trong webpack 4, hãy xoá các tuỳ chọn này 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.nodeEnvDefinePlugin đều hoạt động theo cách tương tự – thay thế tất cả các lần xuất hiện của process.env.NODE_ENV bằng giá trị đã chỉ định. Với cấu hình ở trên:

  1. Webpack sẽ thay thế tất cả các lần xuất hiện 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.');
    }
    
  2. Sau đó, trình rút gọn sẽ xoá tất cả các nhánh if như vậy – vì "production" !== 'production' luôn là 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

Sử dụng mô-đun ES

Cách tiếp theo để giảm kích thước phần phụ trợ là sử dụng các mô-đun ES.

Khi bạn sử dụng các mô-đun ES, webpack có thể thực hiện việc loại bỏ mã không dùng đến. Rút gọn cây là khi trình tạo gói duyệt qua toàn bộ cây phần phụ thuộc, kiểm tra những phần phụ thuộc nào được sử 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 sử dụng:

  1. Bạn viết một tệp có nhiều lệnh xuất, nhưng ứng dụng chỉ sử dụng một trong số đó:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. Webpack hiểu rằng commentRestEndpoint không được sử dụng và không tạo một đ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 */
    })
    
  3. 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})
    

Cách này hoạt động ngay cả với các thư viện nếu chúng được viết bằng mô-đun ES.

Tuy nhiên, bạn không bắt buộc phải sử dụng trình rút gọn tích hợp sẵn của webpack (UglifyJsPlugin) một cách chính xác. Bất kỳ trình rút gọn nào hỗ trợ xoá mã chết (ví dụ: trình bổ trợ Babel Minify hoặc trình bổ trợ Google Closure Compiler) đều sẽ thực hiện được việc này.

Tài liệu đọc thêm

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 bằng JavaScript (ví dụ: không chặn quá trình kết xuất), nhưng các tệp này vẫn chiếm phần lớn băng thông. Sử dụng url-loader, svg-url-loaderimage-webpack-loader để tối ưu hoá các tệp đó trong webpack.

url-loader nội tuyến các tệp tĩnh nhỏ vào ứng dụng. Nếu không có cấu hình, tệp 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ề một 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 URL dữ liệu Base64 và trả về URL này. Thao tác này sẽ đưa hình ảnh vào cùng dòng với mã JavaScript và lưu một 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ã hoá tệp bằng mã hoá URL thay vì mã hoá Base64. Điều này rất 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 các hình ảnh đi qua lớp này. Thư viện này hỗ trợ hình ảnh JPG, PNG, GIF và SVG, vì vậy, chúng ta sẽ sử dụng thư viện này cho tất cả các loại hình ảnh 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 cùng với url-loadersvg-url-loader. Để tránh sao chép và dán 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 hình ảnh SVG), chúng ta sẽ đưa trình tải này vào dưới dạng một quy tắc riêng biệt 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'
      }
    ]
  }
};

Bạn có thể sử dụng các chế độ cài đặt mặc định của trình tải – nhưng nếu muốn định cấu hình thêm, hãy xem các tuỳ chọn trình bổ trợ. Để chọn các tuỳ chọn cần chỉ định, hãy xem hướng dẫn tuyệt vời của Addy Osmani về cách tối ưu hoá hình ảnh.

Tài liệu đọc thêm

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ể 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 khoảng 20 phương thức của lớp này, thì khoảng 65 KB mã rút gọn sẽ không làm được gì cả.

Một ví dụ khác là Moment.js. Phiên bản 2.19.1 của trình bổ trợ này có kích thước mã rút gọn là 223 KB, 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 trong kích thước đó là các tệp bản địa hoá. Nếu bạn không sử dụng Moment.js với nhiều ngôn ngữ, các tệp này sẽ làm tăng kích thước 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 tham khảo kho lưu trữ này!

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, 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, việc này là bắt buộc để tách biệt các mô-đun CommonJS/AMD với nhau. Tuy nhiên, điều này đã làm tăng kích thước và hao tổn hiệu suất cho mỗi mô-đun.

Webpack 2 đã ra mắt 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ác mô-đun ES có thể được đó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 bạn có thể tạo gói như vậy – bằng cách nối chuỗi mô-đun. Dưới đâ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 có thấy sự khác biệt không? Trong gói thuần tuý, mô-đun 0 yêu cầu render từ mô-đun 1. Với tính năng nối mô-đun, bạn chỉ cần thay thế require bằng hàm bắt buộc và xoá mô-đun 1. Gói này có ít mô-đun hơn và ít hao tổn mô-đun 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

Sử dụng externals nếu bạn có cả mã webpack và mã không phải webpack

Bạn có thể có một dự án lớn trong đó một số mã được biên dịch bằng webpack và một số mã không được biên dịch. Giống như một trang web lưu trữ video, trong đó tiện ích trình phát có thể được tạo bằng webpack và trang xung quanh có thể không được tạo:

Ảnh chụp màn hình của một trang web lưu trữ video
(Một trang web lưu trữ video hoàn toàn ngẫu nhiên)

Nếu cả hai đoạn mã đều có các phần phụ thuộc chung, bạn có thể chia sẻ các phần phụ thuộc đó để tránh phải 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 webpack – tuỳ chọn này sẽ thay thế các mô-đun bằng biến hoặc các lệnh nhập bên ngoài khác.

Nếu có phần phụ thuộ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 đặt tên đại diện cho 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, webpack sẽ không gói các gói reactreact-dom. Thay vào đó, các giá trị này sẽ được thay thế bằng nội dung 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 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 webpack 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à các mô-đun đại diện cho 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 này vào define() và làm cho gói này 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 một 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óm tắt

  • Bật chế độ phát hành nếu bạn sử dụng webpack 4
  • Giảm thiểu mã bằng trình rút gọn và trình tải cấp gói
  • Xoá mã chỉ dành cho phát triển bằng cách thay thế NODE_ENV bằng production
  • Sử dụng các mô-đun ES để bật tính năng loại bỏ mã không dùng đến
  • 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 bạn thấy phù hợp