Phân phát mã hiện đại cho các trình duyệt hiện đại để tải trang nhanh hơn

Trong lớp học lập trình này, hãy cải thiện hiệu suất của ứng dụng đơn giản này cho phép người dùng đánh giá các chú mèo ngẫu nhiên. Tìm hiểu cách tối ưu hoá gói JavaScript bằng cách giảm thiểu lượng mã được biên dịch.

Ảnh chụp màn hình ứng dụng

Trong ứng dụng mẫu, bạn có thể chọn một từ hoặc biểu tượng cảm xúc để thể hiện mức độ yêu thích của bạn đối với từng chú mèo. Khi bạn nhấp vào một nút, ứng dụng sẽ hiển thị giá trị của nút đó bên dưới hình ảnh chú mèo hiện tại.

Đo

Bạn nên bắt đầu bằng cách kiểm tra trang web trước khi thêm bất kỳ biện pháp tối ưu hoá nào:

  1. Để xem trước trang web, hãy nhấn vào Xem ứng dụng. Sau đó, nhấn vào biểu tượng Màn hình toàn cảnh toàn màn hình.
  2. Nhấn tổ hợp phím `Ctrl+Shift+J` (hoặc `Command+Option+J` trên máy Mac) để mở DevTools.
  3. Nhấp vào thẻ Mạng.
  4. Chọn hộp đánh dấu Tắt bộ nhớ đệm.
  5. Tải lại ứng dụng.

Yêu cầu kích thước gói ban đầu

Ứng dụng này sử dụng hơn 80 KB! Đã đến lúc tìm hiểu xem các phần của gói có đang được sử dụng hay không:

  1. Nhấn Control+Shift+P (hoặc Command+Shift+P trên máy Mac) để mở trình đơn Command (Lệnh). Trình đơn lệnh

  2. Nhập Show Coverage rồi nhấn Enter để hiển thị thẻ Coverage (Phạm vi bao phủ).

  3. Trong thẻ Phạm vi bao phủ, hãy nhấp vào Tải lại để tải lại ứng dụng trong khi thu thập phạm vi bao phủ.

    Tải lại ứng dụng có mức độ sử dụng mã

  4. Hãy xem xét lượng mã đã sử dụng so với lượng mã đã tải cho gói chính:

    Mức độ sử dụng mã của gói

Hơn một nửa gói (44 KB) thậm chí còn không được sử dụng. Lý do là nhiều mã trong đó bao gồm các polyfill để đảm bảo ứng dụng hoạt động trong các trình duyệt cũ.

Sử dụng @babel/preset-env

Cú pháp của ngôn ngữ JavaScript tuân thủ một tiêu chuẩn có tên là ECMAScript hoặc ECMA-262. Các phiên bản mới hơn của quy cách được phát hành hằng năm và bao gồm các tính năng mới đã vượt qua quy trình đề xuất. Mỗi trình duyệt lớn luôn ở một giai đoạn hỗ trợ các tính năng này.

Các tính năng ES2015 sau đây được sử dụng trong ứng dụng:

Tính năng ES2017 sau đây cũng được sử dụng:

Bạn có thể tìm hiểu mã nguồn trong src/index.js để xem cách sử dụng tất cả các thành phần này.

Tất cả các tính năng này đều được hỗ trợ trong phiên bản Chrome mới nhất, nhưng các trình duyệt khác không hỗ trợ các tính năng này thì sao? Babel, có trong ứng dụng, là thư viện phổ biến nhất dùng để biên dịch mã chứa cú pháp mới hơn thành mã mà các trình duyệt và môi trường cũ có thể hiểu được. Phương thức này thực hiện việc này theo hai cách:

  • Polyfill được đưa vào để mô phỏng các hàm ES2015 trở lên mới hơn để có thể sử dụng API của các hàm đó ngay cả khi trình duyệt không hỗ trợ. Dưới đây là ví dụ về một polyfill của phương thức Array.includes.
  • Trình bổ trợ được dùng để chuyển đổi mã ES2015 (hoặc phiên bản mới hơn) thành cú pháp ES5 cũ. Vì đây là những thay đổi liên quan đến cú pháp (chẳng hạn như hàm mũi tên), nên bạn không thể mô phỏng các thay đổi này bằng polyfill.

Hãy xem package.json để biết những thư viện Babel nào được đưa vào:

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}
  • @babel/core là trình biên dịch Babel cốt lõi. Với cấu hình này, tất cả cấu hình Babel đều được xác định trong .babelrc ở thư mục gốc của dự án.
  • babel-loader bao gồm Babel trong quá trình tạo bản dựng webpack.

Bây giờ, hãy xem webpack.config.js để biết cách đưa babel-loader vào làm quy tắc:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill cung cấp tất cả các polyfill cần thiết cho mọi tính năng ECMAScript mới hơn để các tính năng này có thể hoạt động trong các môi trường không hỗ trợ các tính năng đó. Tệp này đã được nhập ở đầu src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env xác định những phép biến đổi và polyfill cần thiết cho mọi trình duyệt hoặc môi trường được chọn làm mục tiêu.

Hãy xem tệp cấu hình Babel, .babelrc, để biết cách đưa tệp này vào:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

Đây là chế độ thiết lập Babel và webpack. Tìm hiểu cách đưa Babel vào ứng dụng nếu bạn tình cờ sử dụng một trình đóng gói mô-đun khác với webpack.

Thuộc tính targets trong .babelrc xác định những trình duyệt đang được nhắm đến. @babel/preset-env tích hợp với danh sách trình duyệt, nghĩa là bạn có thể tìm thấy danh sách đầy đủ các truy vấn tương thích có thể sử dụng trong trường này trong tài liệu về danh sách trình duyệt.

Giá trị "last 2 versions" biên dịch mã trong ứng dụng cho hai phiên bản mới nhất của mọi trình duyệt.

Gỡ lỗi

Để xem toàn bộ các mục tiêu Babel của trình duyệt cũng như tất cả các phép biến đổi và polyfill có trong đó, hãy thêm trường debug vào .babelrc:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • Nhấp vào Công cụ.
  • Nhấp vào Nhật ký.

Tải lại ứng dụng và xem nhật ký trạng thái Glitch ở cuối trình chỉnh sửa.

Trình duyệt được nhắm mục tiêu

Babel ghi lại một số thông tin chi tiết vào bảng điều khiển về quy trình biên dịch, bao gồm tất cả môi trường mục tiêu mà mã đã được biên dịch.

Trình duyệt được nhắm mục tiêu

Hãy lưu ý cách các trình duyệt không còn hoạt động, chẳng hạn như Internet Explorer, được đưa vào danh sách này. Đây là vấn đề vì các trình duyệt không được hỗ trợ sẽ không có các tính năng mới hơn và Babel sẽ tiếp tục biên dịch cú pháp cụ thể cho các trình duyệt đó. Điều này làm tăng kích thước gói của bạn một cách không cần thiết nếu người dùng không sử dụng trình duyệt này để truy cập vào trang web của bạn.

Babel cũng ghi lại danh sách các trình bổ trợ chuyển đổi được sử dụng:

Danh sách trình bổ trợ được sử dụng

Đó là một danh sách khá dài! Đây là tất cả các trình bổ trợ mà Babel cần sử dụng để chuyển đổi mọi cú pháp ES2015 trở lên thành cú pháp cũ cho tất cả các trình duyệt được nhắm đến.

Tuy nhiên, Babel không hiển thị bất kỳ polyfill cụ thể nào được sử dụng:

Chưa thêm polyfill

Điều này là do toàn bộ @babel/polyfill đang được nhập trực tiếp.

Tải từng polyfill

Theo mặc định, Babel bao gồm mọi polyfill cần thiết cho môi trường ES2015+ hoàn chỉnh khi nhập @babel/polyfill vào một tệp. Để nhập các polyfill cụ thể cần thiết cho trình duyệt mục tiêu, hãy thêm useBuiltIns: 'entry' vào cấu hình.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

Tải lại ứng dụng. Giờ đây, bạn có thể thấy tất cả các polyfill cụ thể được đưa vào:

Danh sách polyfill đã nhập

Mặc dù hiện chỉ bao gồm các polyfill cần thiết cho "last 2 versions", nhưng đây vẫn là một danh sách siêu dài! Lý do là vì các trình bổ trợ polyfill cần thiết cho trình duyệt mục tiêu cho mọi tính năng mới vẫn được đưa vào. Thay đổi giá trị của thuộc tính thành usage để chỉ bao gồm những thuộc tính cần thiết cho các tính năng đang được sử dụng trong mã.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

Với tính năng này, polyfill sẽ tự động được đưa vào khi cần. Điều này có nghĩa là bạn có thể xoá lệnh nhập @babel/polyfill trong src/index.js.

import "./style.css";
import "@babel/polyfill";

Giờ đây, chỉ bao gồm các polyfill bắt buộc cần thiết cho ứng dụng.

Danh sách polyfill được tự động đưa vào

Kích thước gói ứng dụng giảm đáng kể.

Kích thước gói giảm xuống còn 30,1 KB

Thu hẹp danh sách trình duyệt được hỗ trợ

Số lượng mục tiêu trình duyệt được đưa vào vẫn còn khá lớn và không có nhiều người dùng sử dụng các trình duyệt đã ngừng hoạt động như Internet Explorer. Cập nhật cấu hình như sau:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

Xem thông tin chi tiết về gói đã tìm nạp.

Kích thước gói là 30 KB

Vì ứng dụng rất nhỏ, nên những thay đổi này thực sự không có nhiều khác biệt. Tuy nhiên, bạn nên sử dụng tỷ lệ phần trăm thị phần trình duyệt (chẳng hạn như ">0.25%") cùng với việc loại trừ những trình duyệt cụ thể mà bạn chắc chắn người dùng không sử dụng. Hãy xem bài viết "2 phiên bản gần đây nhất" bị coi là có hại của James Kyle để tìm hiểu thêm về vấn đề này.

Sử dụng <script type="module">

Chúng tôi vẫn có thể cải thiện thêm. Mặc dù một số polyfill không dùng đến đã bị xoá, nhưng vẫn có nhiều polyfill đang được phân phối mà không cần thiết cho một số trình duyệt. Bằng cách sử dụng các mô-đun, bạn có thể viết và gửi trực tiếp cú pháp mới hơn đến trình duyệt mà không cần sử dụng bất kỳ polyfill nào không cần thiết.

Mô-đun JavaScript là một tính năng tương đối mới được hỗ trợ trong tất cả trình duyệt lớn. Bạn có thể tạo mô-đun bằng thuộc tính type="module" để xác định các tập lệnh nhập và xuất từ các mô-đun khác. Ví dụ:

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

Nhiều tính năng ECMAScript mới hơn đã được hỗ trợ trong các môi trường hỗ trợ mô-đun JavaScript (thay vì cần Babel). Điều này có nghĩa là bạn có thể sửa đổi cấu hình Babel để gửi hai phiên bản ứng dụng khác nhau đến trình duyệt:

  • Một phiên bản sẽ hoạt động trong các trình duyệt mới hơn hỗ trợ mô-đun và bao gồm một mô-đun hầu như không được biên dịch nhưng có kích thước tệp nhỏ hơn
  • Một phiên bản bao gồm một tập lệnh lớn hơn, được biên dịch sẽ hoạt động trong mọi trình duyệt cũ

Sử dụng mô-đun ES với Babel

Để có các chế độ cài đặt @babel/preset-env riêng biệt cho hai phiên bản của ứng dụng, hãy xoá tệp .babelrc. Bạn có thể thêm chế độ cài đặt Babel vào cấu hình webpack bằng cách chỉ định hai định dạng biên dịch khác nhau cho mỗi phiên bản của ứng dụng.

Bắt đầu bằng cách thêm cấu hình cho tập lệnh cũ vào webpack.config.js:

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

Lưu ý rằng thay vì sử dụng giá trị targets cho "@babel/preset-env", bạn sẽ sử dụng esmodules có giá trị là false. Điều này có nghĩa là Babel bao gồm tất cả các phép biến đổi và polyfill cần thiết để nhắm đến mọi trình duyệt chưa hỗ trợ các mô-đun ES.

Thêm các đối tượng entry, cssRulecorePlugins vào đầu tệp webpack.config.js. Tất cả các tập lệnh này đều được chia sẻ giữa mô-đun và các tập lệnh cũ được phân phát đến trình duyệt.

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

Tương tự như vậy, hãy tạo một đối tượng cấu hình cho tập lệnh mô-đun bên dưới, trong đó legacyConfig được xác định:

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

Điểm khác biệt chính ở đây là đuôi tệp .mjs được dùng cho tên tệp đầu ra. Giá trị esmodules được đặt thành true ở đây, có nghĩa là mã được xuất vào mô-đun này là một tập lệnh nhỏ hơn, ít được biên dịch hơn và không trải qua bất kỳ phép biến đổi nào trong ví dụ này vì tất cả các tính năng được sử dụng đều đã được hỗ trợ trong các trình duyệt hỗ trợ mô-đun.

Ở cuối tệp, hãy xuất cả hai cấu hình trong một mảng.

module.exports = [
  legacyConfig, moduleConfig
];

Giờ đây, việc này sẽ tạo cả một mô-đun nhỏ hơn cho các trình duyệt hỗ trợ mô-đun đó và một tập lệnh được biên dịch lớn hơn cho các trình duyệt cũ.

Các trình duyệt hỗ trợ mô-đun sẽ bỏ qua các tập lệnh có thuộc tính nomodule. Ngược lại, các trình duyệt không hỗ trợ mô-đun sẽ bỏ qua các phần tử tập lệnh có type="module". Điều này có nghĩa là bạn có thể đưa một mô-đun cũng như một phương án dự phòng đã biên dịch vào. Lý tưởng nhất là hai phiên bản của ứng dụng phải nằm trong index.html như sau:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

Các trình duyệt hỗ trợ mô-đun sẽ tìm nạp và thực thi main.mjs và bỏ qua main.bundle.js.. Các trình duyệt không hỗ trợ mô-đun sẽ làm ngược lại.

Điều quan trọng cần lưu ý là không giống như các tập lệnh thông thường, tập lệnh mô-đun luôn được trì hoãn theo mặc định. Nếu muốn tập lệnh nomodule tương đương cũng bị trì hoãn và chỉ được thực thi sau khi phân tích cú pháp, thì bạn cần thêm thuộc tính defer:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

Việc cuối cùng cần làm ở đây là thêm các thuộc tính modulenomodule vào mô-đun và tập lệnh cũ tương ứng, Nhập ScriptExtHtmlWebpackPlugin ở đầu webpack.config.js:

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

Bây giờ, hãy cập nhật mảng plugins trong cấu hình để thêm trình bổ trợ này:

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

Các chế độ cài đặt trình bổ trợ này sẽ thêm thuộc tính type="module" cho tất cả các phần tử tập lệnh .mjs cũng như thuộc tính nomodule cho tất cả các mô-đun tập lệnh .js.

Phân phát mô-đun trong tài liệu HTML

Việc cuối cùng cần làm là xuất cả phần tử tập lệnh cũ và hiện đại vào tệp HTML. Rất tiếc, trình bổ trợ tạo tệp HTML cuối cùng, HTMLWebpackPlugin, hiện không hỗ trợ đầu ra của cả tập lệnh mô-đun và nomodule. Mặc dù có các giải pháp và trình bổ trợ riêng biệt được tạo để giải quyết vấn đề này, chẳng hạn như BabelMultiTargetPluginHTMLWebpackMultiBuildPlugin, nhưng trong hướng dẫn này, chúng ta sẽ sử dụng phương pháp đơn giản hơn là thêm phần tử tập lệnh mô-đun theo cách thủ công.

Thêm nội dung sau vào src/index.js ở cuối tệp:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

Bây giờ, hãy tải ứng dụng trong một trình duyệt hỗ trợ các mô-đun, chẳng hạn như phiên bản Chrome mới nhất.

Mô-đun 5,2 KB được tìm nạp qua mạng cho các trình duyệt mới hơn

Chỉ mô-đun được tìm nạp, với kích thước gói nhỏ hơn nhiều do phần lớn mô-đun này không được biên dịch! Trình duyệt sẽ hoàn toàn bỏ qua phần tử tập lệnh khác.

Nếu bạn tải ứng dụng trên một trình duyệt cũ, thì chỉ tập lệnh lớn hơn, được chuyển đổi với tất cả các polyfill và phép biến đổi cần thiết mới được tìm nạp. Dưới đây là ảnh chụp màn hình cho tất cả các yêu cầu được thực hiện trên phiên bản Chrome cũ (phiên bản 38).

Tệp tập lệnh 30 KB được tìm nạp cho các trình duyệt cũ

Kết luận

Giờ đây, bạn đã hiểu cách sử dụng @babel/preset-env để chỉ cung cấp các polyfill cần thiết cho các trình duyệt được nhắm đến. Bạn cũng biết cách các mô-đun JavaScript có thể cải thiện hiệu suất hơn nữa bằng cách phân phối hai phiên bản chuyển đổi khác nhau của một ứng dụng. Khi đã hiểu rõ cách cả hai kỹ thuật này có thể giúp giảm đáng kể kích thước gói, hãy bắt tay vào tối ưu hoá!