Tự động nén và mã hoá

Hãy đảm bảo việc tạo các nguồn hình ảnh có hiệu suất cao là một phần liền mạch trong quá trình phát triển ứng dụng.

Tất cả cú pháp trong khoá học này – từ việc mã hoá dữ liệu hình ảnh đến mã đánh dấu dày đặc thông tin hỗ trợ hình ảnh thích ứng – đều là các phương thức để máy giao tiếp với máy. Bạn đã khám phá ra một số cách để trình duyệt ứng dụng giao tiếp nhu cầu của nó với máy chủ và máy chủ để phản hồi theo kiểu tương tự. Mã đánh dấu hình ảnh thích ứng (cụ thể là srcsetsizes) có thể mô tả một lượng thông tin gây sốc qua tương đối ít ký tự. Nói một cách tốt hơn là ngắn gọn đó là do thiết kế: việc làm cho những cú pháp này ngắn gọn hơn và vì vậy nhà phát triển dễ phân tích cú pháp hơn có thể khiến trình duyệt khó phân tích cú pháp. Chuỗi càng phức tạp thì càng có nhiều khả năng xảy ra lỗi trình phân tích cú pháp hoặc sự khác biệt ngoài ý muốn về hành vi giữa các trình duyệt.

Cửa sổ mã hoá hình ảnh tự động.

Tuy nhiên, chính đặc điểm khiến những đối tượng này cảm thấy đáng sợ cũng có thể cung cấp cho bạn giải pháp: một cú pháp dễ đọc bằng máy móc là cú pháp được chúng viết dễ dàng hơn. Hầu như bạn đã gặp nhiều ví dụ về mã hoá và nén hình ảnh tự động khi là người dùng web: bất kỳ hình ảnh nào được tải lên web thông qua nền tảng mạng xã hội, hệ thống quản lý nội dung (CMS) và thậm chí ứng dụng email khách hầu như luôn đi qua hệ thống đổi kích thước, mã hoá lại và nén chúng.

Tương tự như vậy, cho dù thông qua các trình bổ trợ, thư viện bên ngoài, công cụ quy trình xây dựng độc lập hay việc sử dụng tập lệnh phía máy khách có trách nhiệm, mã đánh dấu hình ảnh thích ứng sẽ dễ dàng thích ứng với công nghệ tự động hoá.

Đó là 2 vấn đề chính cần quan tâm khi tự động hoá hiệu suất của hình ảnh: quản lý việc tạo hình ảnh (mã hoá, nén và các nguồn thay thế mà bạn sẽ sử dụng để điền thuộc tính srcset) và tạo mã đánh dấu dành cho người dùng. Trong mô-đun này, bạn sẽ tìm hiểu về một số phương pháp phổ biến để quản lý hình ảnh trong quy trình làm việc hiện đại, cho dù dưới dạng giai đoạn tự động trong quy trình phát triển, thông qua khung hoặc hệ thống quản lý nội dung hỗ trợ trang web hay gần như được loại bỏ hoàn toàn bằng một mạng phân phối nội dung chuyên dụng.

Tự động hoá quá trình nén và mã hoá

Bạn sẽ ít có khả năng gặp phải tình huống mà bạn có thể dành thời gian để xác định thủ công mã hoá và mức độ nén lý tưởng cho từng hình ảnh riêng lẻ được dùng cho một dự án. Bạn cũng không nên làm vậy. Việc giữ cho kích thước chuyển hình ảnh càng nhỏ càng tốt , việc tinh chỉnh các chế độ cài đặt nén và lưu lại các nguồn thay thế cho mọi thành phần hình ảnh dành cho trang web phát hành chính thức sẽ gây ra một nút thắt cổ chai lớn trong công việc hằng ngày của bạn.

Như bạn đã tìm hiểu khi đọc về các định dạng hình ảnh và loại nén, phương thức mã hoá hiệu quả nhất cho một hình ảnh sẽ luôn được quy định theo nội dung của hình ảnh. Và như bạn đã tìm hiểu trong phần Hình ảnh thích ứng, kích thước thay thế cần thiết cho nguồn hình ảnh sẽ được xác định theo vị trí của những hình ảnh đó trong bố cục trang. Trong quy trình làm việc hiện đại, bạn sẽ tiếp cận những quyết định này một cách tổng thể thay vì từng quyết định – xác định một tập hợp các giá trị mặc định hợp lý cho hình ảnh, để phù hợp nhất với ngữ cảnh mà hình ảnh được sử dụng.

Khi chọn phương thức mã hoá cho một thư mục hình ảnh, AVIF là yếu tố rõ ràng nhất về chất lượng và dung lượng truyền nhưng khả năng hỗ trợ bị hạn chế, WebP cung cấp tính năng dự phòng hiện đại, được tối ưu hoá và JPEG là chế độ mặc định đáng tin cậy nhất. Kích thước thay thế mà chúng tôi cần tạo ra cho hình ảnh chiếm thanh bên trong bố cục trang sẽ khác nhau rất nhiều so với hình ảnh nhằm chiếm toàn bộ khung nhìn của trình duyệt tại các điểm ngắt cao nhất. Các chế độ cài đặt nén sẽ yêu cầu bạn phải chú ý đến việc làm mờ và nén các cấu phần phần mềm trên nhiều tệp thu được, do đó, tạo ra ít không gian hơn để cắt mọi byte có thể có từ mỗi hình ảnh nhằm đổi lấy quy trình làm việc linh hoạt và đáng tin cậy hơn. Tóm lại, bạn sẽ thực hiện theo cùng một quy trình đưa ra quyết định mà bạn đã hiểu trong khoá học này.

Về bản thân quá trình xử lý, có một số lượng lớn thư viện xử lý hình ảnh nguồn mở cung cấp các phương pháp chuyển đổi, sửa đổi và chỉnh sửa hình ảnh hàng loạt, cạnh tranh về tốc độ, hiệu quả và độ tin cậy. Các thư viện xử lý này sẽ cho phép bạn áp dụng các chế độ cài đặt mã hoá và nén cho toàn bộ thư mục hình ảnh cùng một lúc mà không cần phải mở phần mềm chỉnh sửa hình ảnh, đồng thời giữ lại các nguồn hình ảnh gốc nếu cần điều chỉnh các chế độ cài đặt đó một cách nhanh chóng. Các bản dựng này được dự định chạy trong nhiều ngữ cảnh, từ môi trường phát triển cục bộ cho đến chính máy chủ web — ví dụ: ImageMin tập trung vào việc nén cho Node.js có thể được mở rộng cho phù hợp với các ứng dụng cụ thể thông qua một loạt trình bổ trợ, trong khi ImageMagick đa nền tảng và Sharp có số lượng tính năng đáng kinh ngạc ngay từ đầu.

Các thư viện xử lý hình ảnh này giúp nhà phát triển có thể xây dựng các công cụ dành riêng cho việc tối ưu hoá hình ảnh một cách liền mạch trong quy trình phát triển tiêu chuẩn – đảm bảo rằng dự án của bạn sẽ luôn tham chiếu các nguồn hình ảnh sẵn sàng phát hành với ít chi phí nhất có thể.

Các công cụ và quy trình phát triển tại địa phương

Bạn có thể sử dụng trình chạy tác vụ và trình đóng gói như Grunt, Gulp hoặc Webpack để tối ưu hoá thành phần hình ảnh cùng với các tác vụ phổ biến khác liên quan đến hiệu suất, chẳng hạn như giảm thiểu CSS và JavaScript. Để minh hoạ, hãy cùng xem một trường hợp sử dụng tương đối đơn giản: một thư mục trong dự án của bạn chứa nhiều ảnh chụp, nghĩa là dùng trên một trang web công khai.

Trước tiên, bạn cần đảm bảo mã hoá nhất quán, hiệu quả cho những hình ảnh này. Như đã tìm hiểu trong các mô-đun trước, WebP là một chế độ mặc định hiệu quả cho ảnh chụp xét về cả chất lượng và kích thước tệp. WebP được hỗ trợ tốt, nhưng không được hỗ trợ toàn cầu. Vì vậy, bạn cũng nên thêm bản dự phòng ở dạng JPEG tăng dần. Sau đó, để sử dụng thuộc tính srcset nhằm phân phối hiệu quả các thành phần này, bạn cần tạo nhiều kích thước thay thế cho từng kiểu mã hoá.

Mặc dù đây sẽ là một công việc lặp đi lặp lại và tốn thời gian nếu bạn thực hiện bằng phần mềm chỉnh sửa hình ảnh, nhưng các trình chạy tác vụ như Gulp được thiết kế để tự động hoá chính xác kiểu lặp lại này. Trình bổ trợ gulp-responsive sử dụng Sharp là một trong số nhiều tuỳ chọn đều tuân theo một mẫu tương tự nhau: thu thập tất cả các tệp trong thư mục nguồn, mã hoá lại và nén chúng dựa trên cùng một "chất lượng" được chuẩn hoá mà bạn đã tìm hiểu trong phần Định dạng và nén hình ảnh. Sau đó, các tệp kết quả sẽ được xuất sang một đường dẫn mà bạn xác định, sẵn sàng được tham chiếu trong các thuộc tính src của phần tử img mà người dùng nhìn thấy trong khi vẫn giữ nguyên các tệp gốc.

const { src, dest } = require('gulp');
const respimg = require('gulp-responsive');

exports.webp = function() {
  return src('./src-img/*')
    .pipe(respimg({
      '*': [{
        quality: 70,
        format: ['webp', 'jpeg'],
        progressive: true
      }]
  }))
  .pipe(dest('./img/'));
}

Với quy trình như vậy, môi trường sản xuất sẽ không gây ảnh hưởng gì nếu ai đó trong dự án vô tình thêm ảnh được mã hoá dưới dạng tệp PNG màu thực có kích thước lớn vào thư mục chứa nguồn hình ảnh gốc – bất kể phương thức mã hoá của hình ảnh gốc là gì, tác vụ này sẽ tạo ra WebP hiệu quả và dự phòng JPEG tiến bộ đáng tin cậy, đồng thời ở mức nén có thể dễ dàng điều chỉnh nhanh chóng. Tất nhiên, quá trình này cũng đảm bảo rằng các tệp hình ảnh gốc sẽ được giữ lại trong môi trường phát triển của dự án, nghĩa là các chế độ cài đặt này có thể được điều chỉnh bất cứ lúc nào mà chỉ cần kết quả tự động được ghi đè.

Để xuất nhiều tệp, bạn sẽ truyền nhiều đối tượng cấu hình – tất cả đều giống nhau, ngoại trừ việc thêm khoá width và một giá trị tính bằng pixel:

const { src, dest } = require('gulp');
const respimg = require('gulp-responsive');

exports.default = function() {
  return src('./src-img/*')
    .pipe(respimg({
    '*': [{
            width: 1000,
            format: ['jpeg', 'webp'],
            progressive: true,
            rename: { suffix: '-1000' }
            },
            {
            width: 800,
            format: ['jpeg', 'webp'],
            progressive: true,
            rename: { suffix: '-800' }
            },
            {
            width: 400,
            format: ['jpeg', 'webp'],
            progressive: true,
            rename: { suffix: '-400' },
        }]
        })
    )
    .pipe(dest('./img/'));
}

Trong trường hợp ví dụ trên, hình ảnh gốc (monarch.png) có kích thước lớn hơn 3,3MB. Tệp lớn nhất do tác vụ này tạo (monarch-1000.jpeg) có kích thước khoảng 150 KB. Tệp monarch-400.web nhỏ nhất chỉ có 32KB.

[10:30:54] Starting 'default'...
[10:30:54] gulp-responsive: monarch.png -> monarch-400.jpeg
[10:30:54] gulp-responsive: monarch.png -> monarch-800.jpeg
[10:30:54] gulp-responsive: monarch.png -> monarch-1000.jpeg
[10:30:54] gulp-responsive: monarch.png -> monarch-400.webp
[10:30:54] gulp-responsive: monarch.png -> monarch-800.webp
[10:30:54] gulp-responsive: monarch.png -> monarch-1000.webp
[10:30:54] gulp-responsive: Created 6 images (matched 1 of 1 image)
[10:30:54] Finished 'default' after 374 ms

Tất nhiên, bạn nên kiểm tra kỹ kết quả để xem các cấu phần phần mềm nén có thể xem được hoặc có thể tăng mức nén để tiết kiệm thêm. Vì tác vụ này không phá huỷ nên bạn có thể dễ dàng thay đổi các chế độ cài đặt này.

Tóm lại, để đổi lấy số kilobyte mà bạn có thể tạo ra nhờ tính năng tối ưu hoá vi mô theo cách thủ công một cách cẩn thận, bạn sẽ có được một quy trình không chỉ hiệu quả mà còn bền vững – một công cụ áp dụng liền mạch kiến thức của bạn về thành phần hình ảnh hiệu suất cao cho toàn bộ dự án mà không cần sự can thiệp thủ công.

Mã đánh dấu hình ảnh thích ứng trong thực tế

Việc điền sẵn các thuộc tính srcset thường sẽ là một quy trình thủ công đơn giản, vì thuộc tính này thực sự chỉ thu thập thông tin về cấu hình bạn đã thực hiện khi tạo nguồn. Trong các nhiệm vụ ở trên, chúng tôi đã thiết lập tên tệp và thông tin về chiều rộng mà thuộc tính của chúng tôi sẽ tuân theo:

srcset="filename-1000.jpg 1000w, filename-800.jpg 800w, filename-400.jpg 400w"

Hãy nhớ rằng nội dung của thuộc tính srcset mang tính mô tả chứ không mang tính quy tắc. Việc nạp chồng thuộc tính srcset không có vấn đề gì, miễn là tỷ lệ khung hình của mọi nguồn đều nhất quán. Thuộc tính srcset có thể chứa URI và chiều rộng của mọi lần cắt thay thế do máy chủ tạo ra mà không gây ra bất kỳ yêu cầu không cần thiết nào. Đồng thời, chúng tôi cung cấp càng nhiều nguồn đề xuất cho hình ảnh được kết xuất thì trình duyệt càng có thể điều chỉnh yêu cầu hiệu quả hơn.

Như đã tìm hiểu trong phần Hình ảnh thích ứng, bạn nên sử dụng phần tử <picture> để xử lý liền mạch mẫu dự phòng WebP hoặc JPEG. Trong trường hợp này, bạn sẽ sử dụng thuộc tính type cùng với srcset.

<picture>
  <source type="image/webp" srcset="filename-1000.webp 1000w, filename-800.webp 800w, filename-400.webp 400w">
  <img srcset="filename-1000.jpg 1000w, filename-800.jpg 800w, filename-400.jpg 400w" sizes="…" alt="…">
</picture>

Như bạn đã tìm hiểu, các trình duyệt hỗ trợ WebP sẽ nhận dạng nội dung của thuộc tính type và chọn thuộc tính srcset của phần tử <source> đó làm danh sách hình ảnh đề xuất. Các trình duyệt không nhận dạng image/webp là loại nội dung đa phương tiện hợp lệ sẽ bỏ qua <source> này và thay vào đó sẽ sử dụng thuộc tính srcset của phần tử <img> bên trong.

Có một điều cần cân nhắc nữa về việc hỗ trợ trình duyệt: các trình duyệt không hỗ trợ mã đánh dấu hình ảnh thích ứng vẫn sẽ cần bản dự phòng hoặc có nguy cơ hình ảnh bị hỏng trong ngữ cảnh duyệt web đặc biệt cũ. Do <picture>, <source>srcset đều bị bỏ qua trong các trình duyệt này, nên chúng ta cần chỉ định một nguồn mặc định trong thuộc tính src của <img> bên trong.

Vì việc điều chỉnh tỷ lệ hình ảnh xuống dưới là một cách liền mạch hiển thị và phương thức mã hoá JPEG phổ biến, nên loại JPEG lớn nhất là lựa chọn hợp lý.

<picture>
  <source type="image/webp" srcset="filename-1000.webp 1000w, filename-800.webp 800w, filename-400.webp 400w">
  <img src="filename-1000.jpg" srcset="filename-1000.jpg 1000w, filename-800.jpg 800w, filename-400.jpg 400w" sizes="…" alt="…">
</picture>

Có thể sizes sẽ hơi khó xử lý hơn một chút. Như bạn đã tìm hiểu, sizes nhất thiết phải theo ngữ cảnh – bạn không thể điền thuộc tính này nếu không biết lượng không gian mà hình ảnh sẽ chiếm trong bố cục kết xuất. Để các yêu cầu hiệu quả nhất có thể, thuộc tính sizes chính xác cần có trong mã đánh dấu của chúng tôi tại thời điểm người dùng cuối thực hiện những yêu cầu đó, rất lâu trước khi người dùng yêu cầu các kiểu chi phối bố cục trang. Việc bỏ qua sizes hoàn toàn không chỉ vi phạm thông số kỹ thuật HTML, mà còn dẫn đến hành vi mặc định tương đương với sizes="100vw" – thông báo cho trình duyệt rằng hình ảnh này chỉ bị hạn chế bởi chính khung nhìn, dẫn đến việc chọn các nguồn đề xuất lớn nhất có thể.

Tương tự như với bất kỳ tác vụ phát triển web đặc biệt nặng nề nào, một số công cụ đã được tạo ra để loại bỏ quá trình viết tay các thuộc tính sizes. respImageLint là một đoạn mã thiết yếu, dùng để kiểm tra độ chính xác của các thuộc tính sizes và đưa ra đề xuất cải thiện. Dấu trang hoạt động như một bookmarklet — một công cụ bạn chạy trong trình duyệt, trong khi trỏ đến trang được kết xuất đầy đủ có chứa các phần tử hình ảnh. Trong bối cảnh mà trình duyệt có hiểu biết đầy đủ về bố cục trang, trình duyệt cũng sẽ có nhận thức gần như hoàn hảo về không gian mà một hình ảnh sẽ chiếm trong bố cục đó ở mọi kích thước khung nhìn có thể có.

Báo cáo hình ảnh thích ứng cho thấy kích thước/chiều rộng không khớp.

Công cụ tìm lỗi mã nguồn các thuộc tính sizes chắc chắn sẽ hữu ích, nhưng công cụ này thậm chí còn hữu ích hơn nữa khi đóng vai trò là công cụ tạo bán buôn các thuộc tính này. Như bạn đã biết, cú pháp srcsetsizes dùng để tối ưu hoá các yêu cầu cho thành phần hình ảnh một cách liền mạch về mặt hình ảnh. Mặc dù không nên sử dụng trong phiên bản chính thức, nhưng giá trị phần giữ chỗ sizes mặc định của 100vw là hoàn toàn hợp lý khi đang xử lý bố cục của trang trong môi trường phát triển cục bộ. Sau khi tạo kiểu bố cục, việc chạy respImageLint sẽ cung cấp cho bạn các thuộc tính sizes phù hợp mà bạn có thể sao chép và dán vào mã đánh dấu, ở mức độ chi tiết hơn nhiều so với cách viết thủ công:

Báo cáo hình ảnh thích ứng có các phương diện được đề xuất.

Mặc dù các yêu cầu hình ảnh do mã đánh dấu kết xuất từ máy chủ khởi tạo xảy ra quá nhanh nên JavaScript không thể tạo thuộc tính sizes phía máy khách, nhưng lý do tương tự sẽ không áp dụng nếu các yêu cầu đó được khởi tạo phía máy khách. Ví dụ: dự án Lazysizes cho phép bạn trì hoãn hoàn toàn các yêu cầu hình ảnh cho đến khi bố cục đã được thiết lập, cho phép JavaScript tạo các giá trị sizes cho chúng tôi. Điều này rất thuận tiện cho bạn và đảm bảo các yêu cầu của người dùng đạt hiệu quả cao nhất. Tuy nhiên, xin lưu ý rằng phương pháp này có nghĩa là hy sinh độ tin cậy của mã đánh dấu do máy chủ hiển thị và tính năng tối ưu hoá tốc độ tích hợp sẵn trong trình duyệt. Việc chỉ bắt đầu các yêu cầu này sau khi trang đã hiển thị sẽ có tác động tiêu cực rất lớn đến điểm LCP của bạn.

Tất nhiên, nếu bạn đã phụ thuộc vào một khung hiển thị phía máy khách như React hoặc Vue, thì đó là món nợ bạn đã phải gánh chịu — và trong những trường hợp đó, việc sử dụng Lazysizes có nghĩa là các thuộc tính sizes của bạn có thể bị loại bỏ gần như hoàn toàn. Tốt hơn nữa: vì sizes="auto" trên hình ảnh tải từng phần có được sự đồng thuận và cách triển khai gốc, Lazysizes sẽ thực sự trở thành một polyfill cho hành vi mới được chuẩn hoá đó của trình duyệt.