Tối ưu hóa hiệu suất ở quy mô lớn: Hành trình hiện đại hóa nền tảng CX
Matheus Albuquerque chia sẻ các chiến lược tối ưu hóa một nền tảng trải nghiệm khách hàng (CX) khổng lồ, chuyển đổi từ React 15 và Webpack 1 sang các tiêu chuẩn hiện đại. Bài thuyết trình đề cập đến việc sử dụng codemod dựa trên AST để di chuyển quy mô lớn, triển khai phân phối khác biệt (differential serving) và tận dụng Preact để giảm dung lượng. Ông cũng giải thích cách cân bằng giữa hiệu suất tối ưu với các ràng buộc nghiêm ngặt của các trình duyệt cũ kỹ.

Trong thế giới phát triển web hiện đại, việc tối ưu hóa hiệu suất không chỉ là làm cho ứng dụng tải nhanh hơn, mà còn là một nghệ thuật cân bằng giữa công nghệ tiên tiến và các hệ thống legacy (di sản). Matheus Albuquerque, Staff Front-End Engineer tại Medallia và Google Developer Expert về Web Performance, đã chia sẻ hành trình đầy thách thức nhưng cũng thú vị khi ông và đội ngũ của mình hiện đại hóa một trong những nền tảng Trải nghiệm Khách hàng (CX) lớn nhất thế giới.
Bài viết này sẽ tóm tắt các chiến lược chính được trình bày trong buổi chia sẻ, từ việc nâng cấp các phụ thuộc cũ kỹ, áp dụng kỹ thuật biên dịch mã tự động, cho đến việc tối ưu hóa dung lượng bundle và hỗ trợ đa trình duyệt.
Thách thức hiện đại hóa phụ thuộc
Điểm khởi đầu của dự án là một codebase frontend khá "già cỗi": React 15, Node 10 và Webpack 1. Việc nâng cấp từ Webpack 1 lên 5 không đơn giản là thay đổi số phiên bản, mà là một quá trình dài hơi phải đi tuần tự qua từng phiên bản (từ 1 lên 2, rồi 3, 4 và cuối cùng là 5). Đội ngũ đã đối mặt với sự phức tạp của các plugin không tương thích và sự thay đổi trong cấu hình.
Một bước ngoặt lớn là việc nâng cấp từ React 15 lên React 16. Mặc dù mục tiêu là hiệu suất, nhưng việc này mang lại lợi ích lớn về việc giảm dung lượng bundle (giảm 32% cho React và React-dom). Tuy nhiên, thách thức nằm ở việc xử lý prop-types, vốn là một phần của React 15 nhưng trở thành một gói riêng biệt ở React 16.
Để giải quyết vấn đề này cho hàng ngàn tệp mã, Matheus đã sử dụng jscodeshift — một công cụ của Facebook (Meta) cho phép chạy các tập lệnh chuyển đổi dựa trên AST (Abstract Syntax Tree - Cây cú pháp trừu tượng). Thay vì viết các hàm diff phức tạp hay sửa thủ công, họ sử dụng các codemod có sẵn để tự động hóa việc di chuyển mã. Chiến lược này cho phép họ duy trì song song hai phiên bản (legacy và modern) thông qua các feature flag, đảm bảo sự ổn định cho hàng triệu người dùng cuối.
Chia nhỏ mã nguồn (Code Splitting)
Sau khi hiện đại hóa các thư viện cốt lõi, bước tiếp theo là tối ưu hóa cách tải mã. Đội ngũ áp dụng Code Splitting sử dụng React.lazy và React.Suspense để chia nhỏ một bundle khổng lồ thành các phần nhỏ hơn, chỉ tải khi cần thiết dựa trên loại câu hỏi trong khảo sát.
Tuy nhiên, việc này gặp phải trở ngại lớn: các tập lệnh tùy chỉnh (custom scripts) của khách hàng. Nhiều khách hàng viết JavaScript để sửa đổi giao diện khảo sát, và họ mong đợi các phần tử DOM tồn tại ngay lập tức. Khi code splitting được áp dụng, các phần tử này chưa kịp load đã gây ra lỗi runtime. Giải pháp là phải sandbox và điều phối các script này, đảm bảo chúng chỉ chạy sau khi mọi thứ đã sẵn sàng.
Tận dụng Preact để giảm dung lượng
Một bước đi táo bạo khác là chuyển từ React sang Preact — một thư viện nhẹ hơn nhiều nhờ loại bỏ các mã legacy và hệ thống sự kiện phức tạp của React. Sử dụng lớp tương thích Preact compat, đội ngũ đã giảm kích thước bundle từ 205KB xuống 175KB mà không cần viết lại mã.
Tuy nhiên, sự kết hợp giữa Preact 10 và React.Suspense đã gây ra một lỗi khó hiểu: các phần tử đôi khi load sai thứ tự một cách ngẫu nhiên. Sau nhiều lần thử nghiệm, họ quyết định quay lại sử dụng loadable-components (thêm 2KB vào bundle) thay vì React.lazy, đổi lấy sự ổn định cho hệ thống.
Vấn đề jQuery và trình duyệt cũ
Mặc dù jQuery dường như là công nghệ của quá khứ, nhưng nền tảng này vẫn phải hỗ trợ nó vì nhiều khách hàng sử dụng trong các tùy chỉnh của họ. Để tối ưu hóa, đội ngũ đã xây dựng một quy trình phân tích tĩnh (static analysis) sử dụng Babel để phát hiện xem khách hàng có thực sự sử dụng jQuery hay không, và họ sử dụng nó như thế nào.
Họ cũng sử dụng Proxy trong ES6 để theo dõi các cuộc gọi hàm jQuery tại thời điểm runtime (runtime detection), giúp xác định chính xác các tenant nào thực sự cần jQuery để có thể tắt nó đi cho những ai không dùng, đồng thời giảm thiểu các lỗ hổng bảo mật (CVE) từ các phiên bản jQuery cũ.
Về việc hỗ trợ trình duyệt, đặc biệt là Internet Explorer, đội ngũ đã từ chối sử dụng dịch vụ Polyfill.io (vốn sau này bị tấn công chuỗi cung ứng nghiêm trọng). Thay vào đó, họ áp dụng chiến lược Differential Serving (phân phối khác biệt).
Differential Serving và Runtime Detection
Differential Serving là kỹ thuật tạo ra hai bản bundle: một bản hiện đại (dành cho trình duyệt mới) và một bản legacy (dành cho trình duyệt cũ như IE). Bản hiện đại sử dụng các tính năng ES6+ mà không cần polyfill nặng nề, trong khi bản legacy chứa đầy đủ các polyfill cần thiết.
Để phục vụ đúng bản bundle, họ sử dụng thuộc tính type="module" và nomodule. Tuy nhiên, do không kiểm soát được máy chủ Java monolith phục vụ frontend, họ đã triển khai cơ chế Runtime Detection. Một đoạn mã nhỏ sẽ chạy đầu tiên để kiểm tra khả năng của trình duyệt, sau đó dynamically tải xuống bundle phù hợp.
Kết quả ấn tượng
Những nỗ lực này đã mang lại kết quả vượt mong đợi:
- Trình duyệt hiện đại: Giảm 37% dung lượng bundle (từ 280KB xuống 176KB).
- Trình duyệt cũ (IE): Giảm 25% dung lượng bundle (từ 280KB xuống 223KB).
- Chỉ khoảng 0,2% người dùng phải tải bản bundle lớn hơn (legacy), trong khi 96% người dùng toàn cầu hưởng lợi từ bản tối ưu.
- Các chỉ số Core Web Vitals như FCP (First Contentful Paint), LCP (Largest Contentful Paint) và TTI (Time to Interactive) đều được cải thiện đáng kể.
Kết luận
Bài học rút ra từ hành trình của Medallia là việc hiểu rõ internals (cơ chế bên trong) của các công cụ và tận dụng sức mạnh của Static Analysis (phân tích tĩnh) cũng như công nghệ biên dịch (compiler) là chìa khóa để giải quyết các vấn đề tối ưu hóa quy mô lớn. Đôi khi, để đạt được hiệu suất tối đa, chúng ta không chỉ cần viết code tốt hơn mà còn phải xây dựng các công cụ để tự động hóa việc cải thiện code đó.



