- 3 năm trước, Notion đã cải thiện thành công tốc độ của ứng dụng Notion cho Mac và Windows bằng cách dùng cơ sở dữ liệu SQLite để lưu đệm dữ liệu trên máy khách
- Lần này, họ cũng có thể mang cùng cải tiến đó đến cho người dùng truy cập Notion qua trình duyệt
- Bài viết này phân tích sâu cách họ cải thiện hiệu năng của Notion trên trình duyệt bằng cách sử dụng triển khai
sqlite3 của WebAssembly (WASM)
- Việc sử dụng SQLite đã giúp thời gian điều hướng trang cải thiện 20% trên mọi trình duyệt hiện đại
- Đặc biệt, khác biệt này còn rõ rệt hơn với những người dùng có thời gian phản hồi API chậm do các yếu tố bên ngoài như kết nối Internet
- Ví dụ, thời gian điều hướng trang nhanh hơn 28% với người dùng ở Úc, 31% với người dùng ở Trung Quốc và 33% với người dùng ở Ấn Độ
Công nghệ cốt lõi: OPFS và Web Workers
- Thư viện WASM SQLite sử dụng một API trình duyệt hiện đại có tên Origin Private File System (OPFS) để duy trì dữ liệu giữa các phiên
- OPFS chỉ có thể dùng trong Web Workers
- Có thể hiểu Web Worker là đoạn mã chạy trên một luồng riêng biệt khác với luồng chính, nơi phần lớn JavaScript trong trình duyệt được thực thi
- Notion được đóng gói cùng Webpack, vốn cung cấp cú pháp dễ dùng để tải Web Worker
- Họ thiết lập Web Worker để tạo tệp cơ sở dữ liệu SQLite hoặc tải tệp hiện có bằng OPFS, rồi chạy mã lưu đệm hiện tại bên trong Web Worker này
- Họ dùng thư viện Comlink để quản lý dễ dàng việc truyền thông điệp giữa luồng chính và Worker
Cách tiếp cận dựa trên SharedWorker
- Kiến trúc cuối cùng dựa trên một giải pháp mới do Roy Hashimoto đề xuất trong thảo luận trên GitHub
- Đây là cách tiếp cận cho phép chỉ một tab truy cập SQLite tại một thời điểm trong khi các tab khác vẫn có thể thực thi truy vấn SQLite
- Kiến trúc mới này hoạt động như thế nào?
- Nói ngắn gọn, mỗi tab có một Web Worker chuyên dụng có thể ghi vào SQLite
- Tuy nhiên, trên thực tế chỉ một tab có thể sử dụng Web Worker đó
- SharedWorker chịu trách nhiệm quản lý tab nào là "tab hoạt động"
- Khi tab hoạt động bị đóng, SharedWorker biết rằng nó cần chọn một tab hoạt động mới
- Để thực thi truy vấn SQLite, luồng chính của mỗi tab gửi truy vấn đó đến SharedWorker, rồi SharedWorker chuyển hướng nó tới Worker chuyên dụng của tab hoạt động
- Các tab có thể đồng thời chạy bao nhiêu truy vấn SQLite tùy ý, và chúng luôn được định tuyến tới một tab hoạt động duy nhất
- Mỗi Web Worker truy cập cơ sở dữ liệu SQLite bằng triển khai OPFS SyncAccessHandle Pool VFS, vốn hoạt động trên mọi trình duyệt lớn
Vì sao cách tiếp cận đơn giản không hiệu quả
- Trước khi xây dựng kiến trúc mô tả ở trên, họ đã thử chạy WASM SQLite theo cách đơn giản hơn: mỗi tab có một Web Worker chuyên dụng và mỗi Web Worker ghi vào cơ sở dữ liệu SQLite
- Tuy nhiên, không cách nào trong số đó đủ đáp ứng yêu cầu của Notion để dùng nguyên trạng
Trở ngại #1: cách ly cross-origin
- Để sử dụng OPFS qua
sqlite3_vfs, trang web phải ở trạng thái "cách ly cross-origin"
- Để thêm cách ly cross-origin vào trang, cần thiết lập một số tiêu đề bảo mật nhằm giới hạn các script có thể được tải
- Việc cấu hình các tiêu đề này có thể là một khối lượng công việc đáng kể
- Notion phụ thuộc vào nhiều script bên thứ ba để vận hành các chức năng khác nhau trong hạ tầng web của mình, nên để đạt được cách ly cross-origin hoàn chỉnh, họ sẽ phải yêu cầu từng nhà cung cấp thiết lập tiêu đề mới và thay đổi cách iframe hoạt động — đây là một yêu cầu khó khả thi trong thực tế
- Trong quá trình thử nghiệm, họ vẫn thu được dữ liệu hiệu năng quan trọng bằng cách cung cấp biến thể này cho một nhóm người dùng con thông qua Origin Trials cho SharedArrayBuffer, khả dụng trên trình duyệt Chrome và Edge
- Các Origin Trials này cho phép họ tạm thời vượt qua yêu cầu cách ly cross-origin
Trở ngại #2: vấn đề hỏng dữ liệu
- Khi cung cấp OPFS qua
sqlite3_vfs cho một số ít người dùng, họ bắt đầu gặp lỗi nghiêm trọng ở một số người dùng
- Những người dùng này nhìn thấy dữ liệu sai trên trang
- Ví dụ như bình luận được gán cho nhầm đồng nghiệp, hoặc liên kết đến một trang mới nhưng phần xem trước lại là một trang hoàn toàn khác
- Khi kiểm tra tệp cơ sở dữ liệu của những người dùng bị ảnh hưởng, họ thấy dấu hiệu cho thấy cơ sở dữ liệu SQLite đã bị hỏng theo cách nào đó
- Việc chọn các hàng trong một số bảng cụ thể gây ra lỗi, và khi kiểm tra chính các hàng đó, họ phát hiện các vấn đề nhất quán dữ liệu như nhiều hàng có cùng ID nhưng nội dung khác nhau
- Về việc cơ sở dữ liệu SQLite đã rơi vào trạng thái đó như thế nào, họ suy đoán rằng nguyên nhân là do vấn đề đồng thời
- Bởi có nhiều tab đang mở, và mỗi tab đều có một Web Worker chuyên dụng với kết nối đang hoạt động tới cơ sở dữ liệu SQLite
- Ứng dụng Notion thường xuyên ghi vào bộ nhớ đệm mỗi khi nhận cập nhật từ máy chủ, tức là các tab có thể đồng thời ghi vào cùng một tệp
- Họ vốn đã dùng cách tiếp cận transaction để gom nhiều truy vấn SQLite lại với nhau, nhưng vẫn nghi ngờ mạnh mẽ rằng việc xử lý đồng thời chưa đầy đủ ở phía API OPFS đã gây ra hỏng dữ liệu
- Vì vậy họ bắt đầu ghi log các lỗi hỏng dữ liệu và thử một vài biện pháp vá như thêm Web Locks và chỉ cho phép tab đang được focus ghi vào SQLite
- Những điều chỉnh này có làm giảm tỷ lệ hỏng dữ liệu, nhưng chưa đủ để bật lại tính năng trên lưu lượng production
- Dù vậy, họ vẫn có thể xác nhận rằng vấn đề đồng thời là yếu tố góp phần đáng kể vào hiện tượng hỏng dữ liệu
- Vấn đề này không xảy ra trên ứng dụng desktop của Notion
- Trên nền tảng đó, chỉ có một tiến trình cha duy nhất ghi vào SQLite
- Bạn có thể mở bao nhiêu tab tùy ý trong ứng dụng, nhưng luôn chỉ có một luồng duy nhất truy cập tệp cơ sở dữ liệu
Trở ngại #3: phương án thay thế chỉ có thể chạy trên một tab tại một thời điểm
- Họ cũng đã đánh giá biến thể OPFS SyncAccessHandle Pool VFS
- Biến thể này không cần SharedArrayBuffer, nên có thể dùng trên Safari, Firefox và các trình duyệt khác không có Origin Trial cho SharedArrayBuffer
- Nhược điểm của biến thể này là nó chỉ có thể chạy trên một tab tại một thời điểm
- Nếu một tab khác cố mở cơ sở dữ liệu SQLite tiếp theo, nó sẽ đơn giản là gặp lỗi
- Mặt khác, điều này cũng có nghĩa OPFS SyncAccessHandle Pool VFS không gặp vấn đề đồng thời như biến thể OPFS qua
sqlite3_vfs
- Họ xác nhận điều này khi không phát hiện vấn đề hỏng dữ liệu lúc triển khai cho một nhóm nhỏ người dùng
- Mặt khác nữa, họ không thể phát hành nguyên trạng biến thể này vì muốn mọi tab của người dùng đều được hưởng lợi từ việc lưu đệm
Giải quyết vấn đề
- Việc không có biến thể nào dùng nguyên trạng được đã dẫn họ đến việc xây dựng kiến trúc SharedWorker mô tả ở trên
- Kiến trúc này tương thích với một trong hai biến thể SQLite đó
- Khi dùng biến thể OPFS qua
sqlite3_vfs, chỉ một tab ghi tại một thời điểm nên có thể tránh được vấn đề hỏng dữ liệu
- Khi dùng biến thể OPFS SyncAccessHandle Pool VFS, nhờ SharedWorker mà mọi tab đều có thể dùng lưu đệm
- Sau khi xác nhận kiến trúc này hoạt động với cả hai biến thể, cho thấy cải thiện hiệu năng rõ rệt trong các chỉ số đo lường và không có vấn đề hỏng dữ liệu, đã đến lúc họ phải đưa ra lựa chọn cuối cùng về biến thể sẽ triển khai
- Họ chọn OPFS SyncAccessHandle Pool VFS vì nó không có yêu cầu cách ly cross-origin, nên không cản trở việc phát hành trên các trình duyệt ngoài Chrome và Edge
Giảm thiểu suy giảm hiệu năng
- Khi bắt đầu cung cấp cải tiến này cho người dùng, họ phát hiện một vài điểm suy giảm hiệu năng cần khắc phục, chẳng hạn như thời gian tải bị chậm hơn
Tải trang chậm hơn
- Phát hiện đầu tiên là việc di chuyển giữa các trang Notion nhanh hơn, nhưng tải trang ban đầu lại chậm hơn
- Kết quả profiling cho thấy tải trang thường không bị nghẽn ở khâu lấy dữ liệu
- Mã khởi động ứng dụng của Notion chạy các công việc khác trong khi chờ lời gọi API hoàn tất, như phân tích cú pháp JS và thiết lập ứng dụng, nên nó không hưởng lợi từ lưu đệm SQLite nhiều như điều hướng
- Nguyên nhân chậm đi là vì người dùng phải tải xuống và xử lý thư viện WASM SQLite
- Điều này chặn quy trình tải trang, khiến các tác vụ tải trang khác không thể diễn ra đồng thời
- Vì thư viện này có kích thước vài trăm kilobyte nên phần thời gian tăng thêm hiện rõ trong các chỉ số đo lường
- Để giải quyết, họ điều chỉnh nhẹ cách tải thư viện
- Họ tải WASM SQLite hoàn toàn bất đồng bộ và không để nó chặn tải trang
- Điều này có nghĩa là dữ liệu trang ban đầu hiếm khi được tải từ SQLite
- Nhưng điều đó chấp nhận được, vì họ đánh giá khách quan rằng mức tăng tốc từ việc tải trang ban đầu bằng SQLite không lớn hơn mức chậm do tải thư viện
- Sau khi áp dụng thay đổi, các chỉ số tải trang ban đầu trở nên giống nhau giữa nhóm thử nghiệm và nhóm đối chứng
Thiết bị chậm không hưởng lợi từ lưu đệm
- Một hiện tượng khác họ phát hiện trong các chỉ số là dù thời gian trung vị khi chuyển từ một trang Notion sang trang khác nhanh hơn, thời gian ở bách phân vị thứ 95 lại chậm hơn
- Một số thiết bị nhất định, như điện thoại di động chạy trình duyệt truy cập Notion, không hưởng lợi từ lưu đệm mà thậm chí còn tệ hơn
- Họ tìm thấy lời giải cho điều khó hiểu này trong một cuộc điều tra trước đó do đội mobile thực hiện
- Khi triển khai cơ chế lưu đệm này trong ứng dụng mobile native, một số thiết bị như điện thoại Android cũ đọc từ đĩa rất chậm
- Vì vậy không thể giả định rằng tải dữ liệu từ bộ nhớ đệm trên đĩa sẽ luôn nhanh hơn tải cùng dữ liệu đó từ API
- Kết quả điều tra phía mobile cho thấy trong tải trang đã có sẵn một số logic để cho hai yêu cầu bất đồng bộ (SQLite và API) "đua" với nhau
- Họ chỉ đơn giản triển khai lại logic này trong luồng mã cho cú nhấp điều hướng
- Điều này giúp bách phân vị thứ 95 của thời gian điều hướng trở nên tương đương giữa hai nhóm trong thử nghiệm
Kết luận
- Việc mang các cải tiến hiệu năng của SQLite đến Notion trên trình duyệt đi kèm những khó khăn riêng
- Họ đã phải đối mặt với một loạt vấn đề chưa biết trước, đặc biệt liên quan đến công nghệ mới, và rút ra một số bài học trong quá trình đó:
- Về bản chất, OPFS không xử lý đồng thời một cách thanh nhã. Nhà phát triển cần nhận thức điều này và thiết kế phù hợp
- Web Workers và SharedWorkers (cùng với người họ hàng Service Workers, dù không được nhắc trong bài này) có những khả năng khác nhau, và việc kết hợp chúng khi cần có thể hữu ích
- Tính đến mùa xuân năm 2024, việc triển khai cách ly cross-origin đầy đủ trong một ứng dụng web phức tạp không hề dễ, đặc biệt nếu có sử dụng script bên thứ ba
- Kết quả của việc lưu đệm dữ liệu trong trình duyệt bằng SQLite cho người dùng là họ ghi nhận mức cải thiện 20% về thời gian điều hướng như đã nêu ở trên, mà không thấy các chỉ số khác suy giảm
- Quan trọng là họ không quan sát thấy vấn đề nào do hỏng SQLite gây ra
- Họ cho rằng thành công và tính ổn định của cách tiếp cận cuối cùng này có được là nhờ đội ngũ phụ trách triển khai WASM chính thức của SQLite, cùng Roy Hashimoto, người đã chia sẻ cách tiếp cận thử nghiệm với cộng đồng
6 bình luận
Vì thế nên các dịch vụ cần hợp tác với bên thứ ba phải bật cross-origin isolation ngay từ lần phát hành đầu tiên...
Ồ, rất vui được gặp bạn cometkim ạ
Khi tôi mở trang Notion trên Firefox thì nó bị treo cứng nên không dùng được, không biết có phải vì chuyện này không.. (Ứng dụng Notion vẫn hoạt động tốt nên hiện tại tôi đang dùng cái đó)
Chắc là vậy. Enda cũng chỉ hỗ trợ ghi tệp cục bộ trên Chrome & Edge thôi.
Trước đây tôi cũng từng gặp trường hợp này trên một chiếc laptop Linux cũ; bật ở chế độ riêng tư là được.