Giảm thiểu và nén tải trọng mạng bằng gzip

Lớp học lập trình này khám phá cách cả việc rút gọn và nén gói JavaScript cho ứng dụng sau đây giúp cải thiện hiệu suất trang bằng cách giảm kích thước yêu cầu của ứng dụng.

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

Đo

Trước khi bắt đầu thêm các tính năng tối ưu hoá, bạn nên phân tích trạng thái hiện tại của ứng dụng.

  • Để xem trước trang web, hãy nhấn vào Xem ứng dụng. Sau đó, nhấn vào biểu tượng Toàn màn hình toàn màn hình.

Ứng dụng này cũng được đề cập trong lớp học lập trình "Xoá mã không dùng đến", cho phép bạn bình chọn cho chú mèo con mà bạn yêu thích. 🐈

Bây giờ, hãy xem kích thước của ứng dụng này:

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

Kích thước gói ban đầu trong bảng điều khiển Mạng

Mặc dù đã đạt được nhiều tiến bộ trong lớp học lập trình "Xoá mã không dùng đến" để giảm kích thước gói này, nhưng 225 KB vẫn là khá lớn.

Rút gọn

Hãy xem xét khối mã sau.

function soNice() {
  let counter = 0;

  while (counter < 100) {
    console.log('nice');
    counter++;
  }
}

Nếu hàm này được lưu trong một tệp riêng, thì kích thước tệp sẽ vào khoảng 112 B (byte).

Nếu xoá tất cả khoảng trắng, mã kết quả sẽ có dạng như sau:

function soNice(){let counter=0;while(counter<100){console.log("nice");counter++;}}

Bây giờ, kích thước tệp sẽ vào khoảng 83 B. Nếu bị cắt bớt chiều dài tên biến và sửa đổi một số biểu thức, mã cuối cùng có thể sẽ có dạng như sau:

function soNice(){for(let i=0;i<100;)console.log("nice"),i++}

Kích thước tệp hiện đạt 62 B.

Với mỗi bước, mã sẽ trở nên khó đọc hơn. Tuy nhiên, công cụ JavaScript của trình duyệt sẽ diễn giải từng mã này theo cùng một cách. Lợi ích của việc làm rối mã theo cách này có thể giúp giảm kích thước tệp. 112 B thực sự không phải là nhiều, nhưng kích thước vẫn giảm 50%!

Trong ứng dụng này, webpack phiên bản 4 được dùng làm trình đóng gói mô-đun. Bạn có thể xem phiên bản cụ thể trong package.json.

"devDependencies": {
  //...
  "webpack": "^4.16.4",
  //...
}

Theo mặc định, phiên bản 4 đã rút gọn gói trong chế độ phát hành công khai. Công cụ này sử dụng TerserWebpackPlugin, một trình bổ trợ cho Terser. Terser là một công cụ phổ biến dùng để nén mã JavaScript.

Để biết mã rút gọn trông như thế nào, hãy nhấp vào main.bundle.js khi vẫn ở bảng điều khiển Mạng của DevTools. Bây giờ, hãy nhấp vào thẻ Phản hồi.

Phản hồi rút gọn

Mã ở dạng hoàn chỉnh, được rút gọn và xáo trộn, sẽ xuất hiện trong phần nội dung phản hồi. Để tìm hiểu kích thước của gói nếu không được rút gọn, hãy mở webpack.config.js và cập nhật cấu hình mode.

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

Tải lại ứng dụng và xem lại kích thước gói thông qua bảng điều khiển Mạng của DevTools

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

Đó là một sự khác biệt khá lớn! 😅

Hãy nhớ huỷ bỏ các thay đổi tại đây trước khi tiếp tục.

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

Việc đưa một quy trình rút gọn mã vào ứng dụng phụ thuộc vào các công cụ bạn sử dụng:

  • Nếu sử dụng webpack phiên bản 4 trở lên, bạn không cần làm gì thêm vì mã được rút gọn theo mặc định ở chế độ phát hành chính thức. 👍
  • Nếu bạn sử dụng phiên bản webpack cũ, hãy cài đặt và đưa TerserWebpackPlugin vào quy trình tạo bản dựng webpack. Tài liệu giải thích chi tiết về vấn đề này.
  • Ngoài ra, bạn cũng có thể sử dụng các trình bổ trợ rút gọn khác, chẳng hạn như BabelMinifyWebpackPluginClosureCompilerPlugin.
  • Nếu bạn không sử dụng trình đóng gói mô-đun, hãy sử dụng Terser làm công cụ CLI hoặc đưa trực tiếp công cụ này vào làm phần phụ thuộc.

Nén

Mặc dù thuật ngữ "nén" đôi khi được dùng một cách lỏng lẻo để giải thích cách giảm mã trong quá trình rút gọn, nhưng mã thực sự không được nén theo nghĩa đen.

Nén thường đề cập đến mã đã được sửa đổi bằng thuật toán nén dữ liệu. Không giống như việc rút gọn cung cấp mã hoàn toàn hợp lệ, mã nén cần được giải nén trước khi sử dụng.

Với mỗi yêu cầu và phản hồi HTTP, trình duyệt và máy chủ web có thể thêm tiêu đề để đưa thêm thông tin về thành phần đang được tìm nạp hoặc nhận. Bạn có thể thấy điều này trong thẻ Headers trong bảng điều khiển Mạng của DevTools, trong đó có 3 loại:

  • General (Chung) đại diện cho các tiêu đề chung liên quan đến toàn bộ hoạt động tương tác yêu cầu-phản hồi.
  • Tiêu đề phản hồi hiển thị danh sách các tiêu đề dành riêng cho phản hồi thực tế từ máy chủ.
  • Request Headers (Tiêu đề yêu cầu) hiển thị danh sách các tiêu đề mà ứng dụng đính kèm vào yêu cầu.

Hãy xem tiêu đề accept-encoding trong Request Headers.

Tiêu đề chấp nhận mã hoá

accept-encoding được trình duyệt sử dụng để chỉ định định dạng mã hoá nội dung hoặc thuật toán nén mà trình duyệt hỗ trợ. Có nhiều thuật toán nén văn bản, nhưng chỉ có 3 thuật toán được hỗ trợ ở đây để nén (và giải nén) các yêu cầu mạng HTTP:

  • Gzip (gzip): Định dạng nén được sử dụng rộng rãi nhất cho các hoạt động tương tác giữa máy chủ và ứng dụng. Lớp này được xây dựng dựa trên thuật toán Deflate và được hỗ trợ trong tất cả trình duyệt hiện tại.
  • Giảm thiểu (deflate): Không thường dùng.
  • Brotli (br): Một thuật toán nén mới hơn nhằm cải thiện hơn nữa tỷ lệ nén, nhờ đó có thể giúp tải trang nhanh hơn nữa. Tính năng này được hỗ trợ trong các phiên bản mới nhất của hầu hết trình duyệt.

Ứng dụng mẫu trong hướng dẫn này giống hệt với ứng dụng đã hoàn thành trong lớp học lập trình "Xoá mã không dùng đến", ngoại trừ việc Express hiện được dùng làm khung máy chủ. Trong vài phần tiếp theo, chúng ta sẽ tìm hiểu cả phương thức nén tĩnh và động.

Nén động

Tính năng nén động liên quan đến việc nén các thành phần ngay khi trình duyệt yêu cầu.

Ưu điểm

  • Bạn không cần tạo và cập nhật các phiên bản nén đã lưu của thành phần.
  • Tính năng nén nhanh đặc biệt hiệu quả đối với các trang web được tạo động.

Nhược điểm

  • Quá trình nén tệp ở cấp cao hơn để đạt được tỷ lệ nén tốt hơn sẽ mất nhiều thời gian hơn. Điều này có thể làm giảm hiệu suất khi người dùng chờ các thành phần nén trước khi máy chủ gửi.

Nén động bằng Node/Express

Tệp server.js chịu trách nhiệm thiết lập máy chủ Node lưu trữ ứng dụng.

const express = require('express');

const app = express();

app.use(express.static('public'));

const listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});

Tất cả những gì hiện đang thực hiện là nhập express và sử dụng phần mềm trung gian express.static để tải tất cả các tệp HTML, JS và CSS tĩnh trong thư mục public/ (và các tệp đó được tạo bằng webpack với mọi bản dựng).

Để đảm bảo tất cả tài sản được nén mỗi khi được yêu cầu, bạn có thể sử dụng thư viện phần mềm trung gian nén. Bắt đầu bằng cách thêm thành phần này dưới dạng devDependency trong package.json:

"devDependencies": {
  //...
  "compression": "^1.7.3"
},

Sau đó, nhập tệp này vào tệp máy chủ server.js:

const express = require('express');
const compression = require('compression');

Và thêm lớp này làm phần mềm trung gian trước khi express.static được gắn:

//...

const app = express();

app.use(compression());

app.use(express.static('public'));

//...

Bây giờ, hãy tải lại ứng dụng và xem kích thước gói trong bảng điều khiển Network (Mạng).

Kích thước gói có tính năng nén động

Từ 225 KB xuống còn 61,6 KB! Trong Response Headers, tiêu đề content-encoding cho biết máy chủ đang gửi tệp này được mã hoá bằng gzip.

Tiêu đề mã hoá nội dung

Nén tĩnh

Ý tưởng đằng sau việc nén tĩnh là nén và lưu các thành phần trước.

Ưu điểm

  • Bạn không còn phải lo lắng về độ trễ do mức độ nén cao nữa. Bạn không cần phải làm gì ngay lập tức để nén tệp vì giờ đây, bạn có thể tìm nạp trực tiếp các tệp đó.

Nhược điểm

  • Các tài sản cần được nén với mọi bản dựng. Thời gian tạo bản dựng có thể tăng lên đáng kể nếu bạn sử dụng mức độ nén cao.

Nén tĩnh bằng Node/Express và webpack

Vì tính năng nén tĩnh liên quan đến việc nén các tệp trước, nên bạn có thể sửa đổi chế độ cài đặt webpack để nén các thành phần trong bước tạo bản dựng. Bạn có thể sử dụng CompressionPlugin cho việc này.

Bắt đầu bằng cách thêm thành phần này dưới dạng devDependency trong package.json:

"devDependencies": {
  //...
  "compression-webpack-plugin": "^1.1.11"
},

Giống như mọi trình bổ trợ webpack khác, hãy nhập trình bổ trợ này vào tệp cấu hình, webpack.config.js:

const path = require("path");

//...

const CompressionPlugin = require("compression-webpack-plugin");

Và đưa vào mảng plugins:

module.exports = {
  //...
  plugins: [
    //...
    new CompressionPlugin()
  ]
}

Theo mặc định, trình bổ trợ sẽ nén các tệp bản dựng bằng gzip. Hãy xem tài liệu để tìm hiểu cách thêm các tuỳ chọn sử dụng thuật toán khác hoặc bao gồm/loại trừ một số tệp nhất định.

Khi ứng dụng tải lại và tạo lại, một phiên bản nén của gói chính sẽ được tạo. Mở Glitch Console để xem nội dung bên trong thư mục public/ cuối cùng do máy chủ Node phân phát.

  • Nhấp vào nút Công cụ.
  • Nhấp vào nút Bảng điều khiển.
  • Trong bảng điều khiển, hãy chạy các lệnh sau để chuyển sang thư mục public và xem tất cả tệp của thư mục đó:
cd public
ls

Tệp đầu ra cuối cùng trong thư mục công khai

Phiên bản nén của gói, main.bundle.js.gz, hiện cũng được lưu tại đây. CompressionPlugin cũng nén index.html theo mặc định.

Việc tiếp theo cần làm là yêu cầu máy chủ gửi các tệp đã nén này bất cứ khi nào phiên bản JS gốc của chúng được yêu cầu. Bạn có thể thực hiện việc này bằng cách xác định một tuyến mới trong server.js trước khi các tệp được phân phát bằng express.static.

const express = require('express');
const app = express();

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.gz';
  res.set('Content-Encoding', 'gzip');
  next();
});

app.use(express.static('public'));

//...

app.get được dùng để cho máy chủ biết cách phản hồi yêu cầu GET cho một điểm cuối cụ thể. Sau đó, một hàm gọi lại được dùng để xác định cách xử lý yêu cầu này. Tuyến đường này hoạt động như sau:

  • Việc chỉ định '*.js' làm đối số đầu tiên có nghĩa là đối số này sẽ hoạt động cho mọi điểm cuối được kích hoạt để tìm nạp tệp JS.
  • Trong lệnh gọi lại, .gz được đính kèm vào URL của yêu cầu và tiêu đề phản hồi Content-Encoding được đặt thành gzip.
  • Cuối cùng, next() đảm bảo rằng trình tự tiếp tục với bất kỳ lệnh gọi lại nào có thể là lệnh gọi tiếp theo.

Sau khi ứng dụng tải lại, hãy xem lại bảng điều khiển Network.

Giảm kích thước gói bằng tính năng nén tĩnh

Giống như trước đây, kích thước gói giảm đáng kể!

Kết luận

Lớp học lập trình này đã đề cập đến quy trình rút gọn và nén mã nguồn. Cả hai kỹ thuật này đều đang trở thành chế độ mặc định trong nhiều công cụ hiện có. Vì vậy, điều quan trọng là bạn phải tìm hiểu xem chuỗi công cụ của mình có hỗ trợ các kỹ thuật này hay không hoặc bạn có nên tự bắt đầu áp dụng cả hai quy trình hay không.