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 xếp hạng 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 sao chép.

Ả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 để truyền tải mức độ yêu thích của bạn đối với mỗi 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 View App (Xem ứng dụng), sau đó nhấn vào Fullscreen 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

Hơn 80 KB được dùng cho ứng dụng này! Đã đến lúc tìm hiểu xem liệu 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. Menu lệnh

  2. Nhập Show Coverage và 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ã được 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í chưa đượ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 ứ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 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 các tính năng mới đã vượt qua quy trình đề xuất. Mỗi trình duyệt chính 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:

Vui lòng tìm hiểu sâu hơn về mã nguồn trong src/index.js để xem cách tất cả những thuộc tính này được sử dụng.

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ợ những tính năng này 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 thành 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:

  • Polyfills được đưa vào để mô phỏng các hàm ES2015+ mới hơn, nhờ đó, bạn có thể sử dụng các API của các API đó ngay cả khi trình duyệt không hỗ trợ API đó. Dưới đây là ví dụ về polyfill của phương thức Array.includes.
  • Trình bổ trợ dùng để chuyển đổi mã ES2015 (trở lê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 không thể mô phỏng chúng bằng polyfill.

Xem package.json để biết những thư viện CameraX có sẵn:

"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 CameraX chính. Bằng cách này, tất cả cấu hình Jetpack sẽ được xác định trong .babelrc ở gốc của dự án.
  • babel-loader bao gồm CameraX 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 để những tính năng này có thể hoạt động trong môi trường không hỗ trợ. 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 CameraX, .babelrc, để biết cách bao gồm tệp này:

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

Đây là quá trình thiết lập Bundle và webpack. Tìm hiểu cách đưa nỗ lực vào ứng dụng của bạn nếu bạn sử dụng một trình 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 mục tiêu. @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ể dùng trong trường này ở tài liệu về danh sách trình duyệt.

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

Gỡ lỗi

Để có cái nhìn toàn diện về tất cả các mục tiêu nỗ lực của trình duyệt, cũng như tất cả các phép biến đổi và polyfill có trong phần biến đổi, 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 nhiễu ở cuối trình chỉnh sửa.

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

Bumblebee ghi lại một số thông tin chi tiết về quá trình biên dịch vào bảng điều khiển, 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à 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à nỗ lực của JUnit tiếp tục chuyển đổi cú pháp cụ thể cho các trình duyệt đó. Việc này không cần thiết làm tăng kích thước gói của bạn 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.

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

Danh sách trình bổ trợ đã dùng

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

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

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

Đ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, JUnit bao gồm mọi polyfill cần thiết cho một môi trường ES2015 trở lên hoàn chỉnh khi @babel/polyfill được nhập vào một tệp. Để nhập các 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 polyfill cụ thể đi kèm:

Danh sách polyfills đã nhập

Mặc dù hiện chỉ có các đoạn mã polyfill cần thiết cho "last 2 versions", nhưng đây vẫn là một danh sách quá dài! Điều này là do các polyfill cần thiết cho 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 thành usage để chỉ thêm những tính năng 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"
      }
    ]
  ]
}

Nhờ vậy, các đoạn mã polyfill sẽ được tự động thêm vào khi cần. Đ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, chỉ các polyfill cần thiết cho ứng dụng mới được đưa vào.

Tự động thêm danh sách polyfills

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 cung cấp 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,0 KB

Vì ứng dụng rất nhỏ nên thực sự không có nhiều khác biệt với những thay đổi này. Tuy nhiên, bạn nên sử dụng tỷ lệ phần trăm thị phần trên trình duyệt (chẳng hạn như ">0.25%") cùng với việc loại trừ các trình duyệt cụ thể mà bạn tin rằng người dùng không sử dụng. Hãy xem bài viết "2 phiên bản mới nhất" bị coi là có 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 điều cần cải thiện. Mặc dù một số polyfill không được sử dụng đã bị loại bỏ, nhưng có nhiều polyfill đang được vận chuyển không cần thiết cho một số trình duyệt. Khi sử dụng các mô-đun, cú pháp mới có thể được ghi và chuyển trực tiếp đến trình duyệt mà không cần sử dụng các polyfills 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ả các trình duyệt chính. 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ợ các mô-đun JavaScript (thay vì cần có đối tác Video.) Điều này có nghĩa là bạn có thể sửa đổi cấu hình CameraX để gửi hai phiên bản khác nhau của ứng dụng đế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 có hỗ trợ các mô-đun và bao gồm một mô-đun phần lớn chưa được chuyển mã nhưng có kích thước tệp nhỏ hơn
  • Phiên bản bao gồm tập lệnh lớn hơn và được sao chép. Tệp này sẽ hoạt động trong mọi trình duyệt cũ

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

Để 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 các chế độ cài đặt JUnit vào cấu hình gói web 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ì 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à Bumblebee 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 đế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ả những tập lệnh này đều được chia sẻ giữa cả mô-đun và tập lệnh cũ được phân phát cho 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 config cho tập lệnh mô-đun bên dưới nơi xác định legacyConfig:

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 sử dụng cho tên tệp đầu ra. Giá trị esmodules được đặt thành true ở đây, nghĩa là mã được xuất vào mô-đun này là một tập lệnh nhỏ hơn, được biên dịch ít 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ả tính năng được 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 duy nhất.

module.exports = [
  legacyConfig, moduleConfig
];

Giờ đây, công 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 đó lẫn tập lệnh đã sao chép 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 tập lệnh có thuộc tính nomodule. Ngược lại, những trình duyệt không hỗ trợ mô-đun sẽ bỏ qua các phần tử tập lệnh bằng type="module". Điều này có nghĩa là bạn có thể bao gồm một mô-đun cũng như một dự phòng đã biên dịch. Tốt nhất là hai phiên bản của ứng dụng nên 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 tìm nạp và thực thi main.mjs đồng thời bỏ qua main.bundle.js. Những 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 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ỉ thực thi sau khi phân tích cú pháp, bạn sẽ 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 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ợ sau:

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 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ả mô-đun tập lệnh .js.

Mô-đun phân phát 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 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ả đầu ra của cả tập lệnh mô-đun và tập lệnh không mô-đun. Mặc dù đã có giải pháp và trình bổ trợ riêng biệt để giải quyết vấn đề này, chẳng hạn như BabelMultiTargetPluginHTMLWebpackMultiBuildPlugin, một 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 phần 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 vào một trình duyệt hỗ trợ các mô-đun, chẳng hạn như Chrome phiên bản 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 là được tìm nạp, với kích thước gói nhỏ hơn nhiều do phần lớn chưa chuyển mã! Trình duyệt sẽ bỏ qua hoàn toàn 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 mã 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 đư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 polyfill cần thiết 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 chuyển 2 phiên bản sao chép khác nhau của một ứng dụng. Với sự hiểu biết rõ ràng về cách cả hai kỹ thuật này có thể giúp giảm đáng kể kích thước gói của bạn, hãy tiếp tục và tối ưu hóa!