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 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 từng chú mèo. Khi bạn nhấp vào một nút, ứng dụng sẽ hiện 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 cứ phương án 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 Toàn màn hình toàn màn hình.
  2. Nhấn tổ hợp phím "Control + Shift + J" (hoặc "Command+Option+J" trên máy Mac) để mở Công cụ cho nhà phát triển.
  3. Nhấp vào thẻ Mạng.
  4. Chọn hộp kiểm 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 có kích thước trên 80 KB! Đã đến lúc để tìm hiểu xem có phải các phần của gói không đượ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. Trình đơn lệnh

  2. Nhập Show Coverage rồi nhấn Enter để hiển thị thẻ 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 mức độ phù hợp.

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

  4. Hãy xem lượng mã đã được 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í không được sử dụng. Nguyên nhân là do rất nhiều mã trong đó chứa các đoạn mã polyfill để đảm bảo rằng ứng dụng hoạt động được trên các trình duyệt cũ.

Dùng @babel/preset-env

Cú pháp của ngôn ngữ JavaScript tuân theo một tiêu chuẩn có tên là ECMAScript hay 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 những tính năng mới đã vượt qua được quy trình đề xuất. Mỗi trình duyệt chính luôn ở một giai đoạn khác nhau để hỗ trợ các tính năng này.

Các tính năng sau đây của ES2015 được dùng trong ứng dụng này:

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

Vui lòng tìm hiểu kỹ hơn về mã nguồn trong src/index.js để xem cách sử dụng tất cả những mã 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òn các trình duyệt khác không hỗ trợ thì sao? Babel, có trong ứng dụng, là thư viện phổ biến nhất được dùng để biên dịch mã có chứa cú pháp mới hơn vào mã mà các trình duyệt và môi trường cũ có thể hiểu được. Việc này được thực hiện theo 2 cách:

  • Polyfill được đưa vào để mô phỏng các hàm ES2015+ mới hơn để bạ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à một 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 (trở lên) thành cú pháp ES5 cũ hơn. 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 chúng bằng đoạn mã polyfill.

Xem package.json để biết các thư viện adb nào được bao gồm:

"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 adb cốt lõi. Bằng cách này, tất cả các cấu hình adb đều được xác định trong .babelrc ở cấp độ gốc của dự án.
  • babel-loader bao gồm Squarespace trong quy trình xây dựng gói web.

Bây giờ, hãy xem webpack.config.js để xem cách babel-loader được đưa vào dưới dạng 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 môi trường không hỗ trợ chúng. Mục này đã được nhập ở đầu src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env xác định các phép biến đổi và đoạn mã 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 adb, .babelrc, để biết cách bao gồm tệp này:

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

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

Thuộc tính targets trong .babelrc xác định trình duyệt nào đang được nhắm đến. @babel/preset-env tích hợp với danh sách trình duyệt, tức 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" sẽ chuyển mã trong ứng dụng cho 2 phiên bản gần đây nhất của mọi trình duyệt.

Gỡ lỗi

Để có thông tin đầy đủ về tất cả các mục tiêu adb của trình duyệt cũng như tất cả các biến đổi và đoạn mã 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 Tools (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 trục trặc ở cuối trình chỉnh sửa.

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

Squarespace ghi lại một số thông tin chi tiết vào bảng điều khiển về quá trình biên dịch, bao gồm tất cả cá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 ngừng hoạt động, chẳng hạn như Internet Explorer, được đưa vào danh sách này. Đây là một vấn đề vì các trình duyệt không được hỗ trợ sẽ không được thêm các tính năng mới hơn và Squarespace sẽ tiếp tục chuyển đổi cú pháp cụ thể cho các trình duyệt đó. Việc 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 trang web của bạn.

Squarespace 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à Squarespace cần sử dụng để biến đổi bất kỳ cú pháp ES2015+ nào sang cú pháp cũ hơn cho tất cả các trình duyệt được nhắm mục tiêu.

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

Chưa thêm đoạn mã polyfill

Lý do là toàn bộ @babel/polyfill đang được nhập trực tiếp.

Tải từng tệp polyfill riêng lẻ

Theo mặc định, adb sẽ bao gồm mọi đoạn polyfill cần thiết cho môi trường ES2015+ hoàn chỉnh khi @babel/polyfill được nhập vào tệp. Để nhập các đoạn mã polyfill cụ thể cần thiết cho các 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 đoạn polyfill cụ thể trong đó:

Danh sách các thành phần polyfill đã nhập

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

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

Nhờ vậy, đoạn mã polyfill sẽ tự động được thêm vào khi cần thiết. Điều này có nghĩa là bạn có thể xoá dữ liệu nhập @babel/polyfill trong src/index.js.

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

Hiện tại, Google chỉ thêm các polyfill bắt buộc cho hồ sơ đăng ký.

Danh sách các thành phần polyfill tự động được đư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 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ác cấu hình như sau:

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

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

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

Vì ứng dụng rất nhỏ nên thực sự không có gì khác biệt với những thay đổi này. Tuy nhiên, bạn nên sử dụng một 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ừ một số trình duyệt mà bạn tin rằng người dùng của mình sẽ 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à gây hại của James Kyle để tìm hiểu thêm về điều này.

Dùng <script type="module">

Vẫn còn nhiều khía cạnh cần cải thiện. Mặc dù một số polyfill không dùng đến đã bị xoá, nhưng có nhiều polyfill đang được vận chuyển và 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, cú pháp mới hơn có thể được ghi và chuyển trực tiếp đến trình duyệt mà không cần sử dụng bất kỳ đoạn mã polyfill không cần thiết nào.

Mô-đun JavaScript là một tính năng tương đối mới được hỗ trợ trong tất cả các trình duyệt chính. Bạn có thể tạo các mô-đun bằng cách sử dụ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 đã được hỗ trợ trong các môi trường hỗ trợ các mô-đun JavaScript (thay vì cần đến adb). Điều này có nghĩa là bạn có thể sửa đổi cấu hình adb để 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ỗ trợ mô-đun và bao gồm một mô-đun phần lớn chưa được dịch nhưng có kích thước tệp nhỏ hơn
  • Phiên bản bao gồm tập lệnh đã dịch lớn hơn, hoạt động được trong bất kỳ trình duyệt cũ nào

Sử dụng các mô-đun ES với JDK

Để có các chế độ cài đặt @babel/preset-env riêng biệt cho 2 phiên bản của ứng dụng, hãy xoá tệp .babelrc. Bạn có thể thêm các chế độ cài đặt adb vào cấu hình gói web bằng cách chỉ định 2 định dạng biên dịch khác nhau cho từng 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
}

Xin lưu ý rằng thay vì dùng giá trị targets cho "@babel/preset-env", hệ thống sẽ dùng esmodules có giá trị false. Điều này có nghĩa là adb sẽ bao gồm tất cả các phép biến đổi và polyfill cần thiết để nhắm mục tiêu 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 được chia sẻ giữa mô-đun và 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"})
];

Bây giờ, tương tự, 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 định nghĩa:

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, tức 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ỳ biến đổi nào trong ví dụ này vì tất 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, xuất cả hai cấu hình trong một mảng.

module.exports = [
  legacyConfig, moduleConfig
];

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

Những trình duyệt hỗ trợ mô-đun bỏ qua 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". Tức là bạn có thể bao gồm một mô-đun cũng như một bản dự phòng được biên dịch. Tốt nhất là 2 phiên bản của ứng dụng nên ở trong index.html như sau:

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

Những trình duyệt hỗ trợ các mô-đun tìm nạp và thực thi main.mjs, đồng thời 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, các tập lệnh mô-đun luôn bị trì hoãn theo mặc định. Nếu muốn tập lệnh nomodule tương đương cũng được 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 bạn 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 thêm thuộc tính type="module" cho mọi phần tử của tập lệnh .mjs cũng như thuộc tính nomodule cho mọi mô-đun tập lệnh .js.

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

Việc cuối cùng bạn cần làm là xuất cả phần tử tập lệnh cũ và hiện đại sang 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ợ kết quả của cả tập lệnh mô-đun và tập lệnh không mô-đun. Mặc dù có nhiều giải pháp và trình bổ trợ riêng được tạo ra để giải quyết vấn đề này, chẳng hạn như BabelMultiTargetPluginHTMLWebpackMultiBuildPlugin, nhưng chúng tôi vẫn sử dụng phương pháp đơn giản hơn để thêm phần tử tập lệnh mô-đun theo cách thủ công cho mục đích của hướng dẫn này.

Thêm các dòng 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ỉ có 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 không được chuyển mã! Trình duyệt 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 trình duyệt cũ hơn, thì chỉ có tập lệnh đã dịch lớn hơn, chứa tất cả các giá trị polyfill và 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 đưa ra trên phiên bản Chrome cũ (phiên bản 38).

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

Kết luận

Bây giờ, bạn đã hiểu cách sử dụng @babel/preset-env để chỉ cung cấp các đoạn mã polyfill cần thiết cho các trình duyệt được nhắm mục tiêu. 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 truyền hai phiên bản đã dịch 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 tiếp tục và tối ưu hoá!