Board mượt ở mọi cỡ, từ vài chục tới 6.000 card
Khi một board có hàng trăm ảnh, kéo hay zoom bị khựng, nhất là trên . Board 500 ảnh là ca đầu tiên mình đã giải xong phần vẽ. Trang này đặt một đích chung cho mọi board, đi qua bảy chặng mà người dùng gặp (mở board, di chuyển, ảnh nét dần, rê chuột, sửa, cùng sửa, máy yếu), và nói rõ chặng nào đã đạt, chặng nào còn chưa đo, rồi làm gì tiếp theo thứ tự nào. Chữ có gạch chân chấm là thuật ngữ: rê chuột hoặc chạm vào để xem giải thích ngay tại chỗ.
Ba con số đích là đề xuất ngày 06/11/2026, chưa chốt. Chúng áp cho mọi cỡ board và mọi mức zoom, đo trên máy bị làm chậm 4 lần.
Mục tiêu
1Đích cho mọi cỡ board
Bốn nguyên tắc đứng sau mọi quyết định ở trang này:
- Đặt hạn theo từng khung hình. Màn hình 60 hình mỗi giây cho 16,7 ms mỗi hình; màn hình 120 hình (ProMotion của Mac, iPad) chỉ còn 8,3 ms. Đo cả trung bình lẫn việc dài nhất.
- Tốn theo thứ đang thấy, không theo cả board. Board 6.000 card mà màn hình chỉ thấy 40 card thì chỉ được tốn cho khoảng 40 card, cộng một bản tóm tắt cho phần còn lại.
- Chuyển động trước, độ nét sau. Trong lúc kéo hay zoom không làm việc nặng: tải ảnh, , đổi cỡ ảnh, dựng lại card. Dừng tay thì mới bù.
- Luôn có đường lui. Cách vẽ cũ được giữ vĩnh viễn để quay về. Bật theo tài khoản, rồi theo phần trăm người dùng. Bước nào cũng có số đo trước và sau.
Bảng dưới so ba thứ cho từng cỡ board: hôm nay người dùng đang thấy gì, cách vẽ mới đã đo được gì, và đích.
| Board | Hôm nay (cách vẽ cũ, người dùng đang dùng) | Cách vẽ mới, đã đo | Đích (đề xuất) |
|---|---|---|---|
| Board toàn ảnh, tới 200 card | Máy yếu bắt đầu giật từ khoảng 180 card. 200 card, máy yếu, kéo: bỏ 16,6% khung, bận 971 ms/s. | khi zoom xa: máy yếu chịu được khoảng 535 card. | Máy mạnh bỏ 0%, máy yếu ≤ 1%. |
| Board 500 ảnh | Máy yếu, kéo: bỏ 65%, bận 908 ms/s. | Lớp GPU khi zoom xa: bỏ 0–2%, bận 93–234 ms/s (phần 3–13). | Bỏ ≤ 2%, bận ≤ 150 ms/s, ở mọi mức zoom. |
| Board lẫn chữ, Section, đường nối | Máy yếu bắt đầu giật từ khoảng 60 card. | Lớp GPU không cứu được: vẫn khoảng 60, vì các hộp chữ còn lại vẫn nặng. Cần . | Như board toàn ảnh. |
| Board 6.251 card | Chưa đo trên cách vẽ cũ. | Luồng phụ: luồng chính bận khoảng 25 ms/s, bỏ 0–2,1%. Trừ một ô: zoom 0,85 với ảnh nét nhất trên WebGL2 bỏ 19–59% (phần 14). | Bỏ ≤ 2% ở mọi mức zoom, luồng phụ không trễ khung nào. |
"Máy yếu" ở mọi bảng là một máy Mac M4 bị làm chậm người thợ chính 4 lần bằng công cụ của Chrome. Cách làm chậm này không làm chậm và bộ nhớ, nên máy yếu thật có thể tệ hơn (phần 19). Nguồn: docs/research/canvas-gpu-rendering-2026-10.md, _docs/reports/gav-265-measure-final/REPORT.md, _docs/reports/gav-289-spike/sweep/tables.md.
Hành trình
2Bảy chặng người dùng gặp
"Board mượt" không chỉ là lúc kéo. Một người mở board, đi quanh, chờ ảnh nét, rê chuột lên card, sửa prompt, thấy người khác sửa cùng lúc, và có thể đang dùng một laptop cũ. Mỗi chặng dưới đây có cùng bốn dòng: hiện trạng, đích, cách làm, và task.
1. Mở board
Hiện trạng. Trang đọc board từ cơ sở dữ liệu (Firestore, chờ tối đa 4 giây), vào phòng realtime, rồi tải ảnh cho vùng đang thấy. Chưa ai đo từ lúc bấm mở tới lúc thấy board, ở bất kỳ cỡ nào.
Đích. Board 500 card: thấy bố cục trong 1,5 giây, ảnh trong màn hình nét trong 3 giây. Board 6.000 card: thấy bố cục trong 3 giây. Không lần đứng hình nào quá 200 ms. Đo trên mạng 4G nhanh.
Cách làm. Đo trước. Rồi hiện ngay một ảnh tóm tắt cả board (đã có code dựng ảnh thu nhỏ board), nạp card trong màn hình trước, phần còn lại sau.
Task. T-open, chưa tạo. Việc đầu tiên: kiểm board lớn có vượt giới hạn 1 MiB mỗi bản ghi của Firestore không (phần 16).
2. Nhìn và di chuyển
Hiện trạng. Board 500 ảnh khi zoom xa đã giải xong (phần 3–13), nhưng còn tắt. Luồng phụ vẽ cả board ở mọi mức zoom: board chỉ xem đã chạy trên production cho một tài khoản, board sửa được đã merge, đang chờ lên production.
Đích. Như bảng ở phần 1.
Cách làm. Làm nốt các bước của luồng phụ, thêm bước 7 cho ô zoom đang fail. Ở cách vẽ cũ còn một chỗ tốn: mỗi khung hình, vị trí camera vẫn đi qua React.
Task. GAV-289 bước 3–7 (phần 18).
3. Ảnh nét dần
Hiện trạng. Mỗi ảnh có ba cỡ, chọn theo cỡ card trên màn hình, chỉ đổi lên. Chỉ khi lớp GPU đang vẽ thì việc đổi cỡ mới đợi bạn dừng tay; ở cách vẽ cũ, ảnh đổi ngay giữa cú zoom (phần 17).
Đích. Một cú zoom từ 12% lên 100%: không ô nào trống, không đổi ảnh nào khi đang chuyển động, ảnh trong màn hình nét lại trong 0,5 giây sau khi dừng tay.
Cách làm. Bốn quy tắc ở phần 17. Các con số đang được nghiên cứu, chưa chốt.
Task. T-img và bước 7 của GAV-289, chưa tạo.
4. Rê chuột và thanh công cụ
Hiện trạng. GAV-303: thanh công cụ chỉ hiện khi chuột dừng 150 ms và đi chậm hơn 1 px mỗi ms, ẩn sau 300 ms. Còn một lỗi: chuột chỉ lướt qua thanh công cụ vẫn bị tính là đã dùng. GAV-309 (đã merge): rê chuột lên card không còn làm mờ ảnh khi lớp GPU bật.
Đích. Thanh công cụ không bao giờ hiện khi chuột chỉ lướt qua. Tìm card dưới chuột trong 1 ms ở board 6.000 card. Rê chuột không làm đổi điểm ảnh nào của ảnh.
Cách làm. Một lưới tra cứu vị trí card (đã có trong thiết kế bước 2 của luồng phụ). Áp cùng luật hover cho cả cách vẽ cũ và mới, kèm một bài test "rê chuột không làm mờ" chung.
Task. GAV-303 (MR !565, sửa vòng 2), GAV-309 (MR !574, đã merge).
5. Sửa nội dung
Hiện trạng. Khi luồng phụ vẽ board, card đang sửa là card thật đặt đúng chỗ trên tấm vẽ. Chữ phải xuống dòng y hệt nhau giữa hai cách vẽ, và bộ gõ tiếng Việt phải chạy đúng.
Đích. Card không nhảy khi bắt đầu hay kết thúc sửa (lệch 0 px). Kéo board trong lúc sửa, ô sửa không trôi khung nào. Gõ Telex/VNI đúng trên Chrome, Safari, Firefox. Phím gõ hiện ra trong 1 khung hình.
Cách làm. Bước 3 của luồng phụ. Safari cần thử bằng tay vì máy đo tự động chưa giữ được focus.
Task. GAV-289 bước 3, đang làm.
6. Nhiều người cùng sửa
Hiện trạng. Thao tác được gom 120 ms rồi mới gửi; sửa từ người khác gom 50 ms rồi áp một lần. Định kỳ phòng ghi một bản lưu chứa toàn bộ board. Chỉ chủ board dọn được nhật ký thao tác. Đã gặp: 8/306 board mất card lúc vào phòng (đo 24/08), một phòng tích 423 nghìn thao tác chưa dọn. Chưa có số đo nào ở 500 hay 6.000 card với nhiều người.
Đích. Sửa của người khác hiện ra trong 200 ms (95% số lần). Vào board 6.000 card trong 3 giây, không mất card. Ghi bản lưu board 6.000 card trong 5 giây, không làm đứng màn hình. Nhật ký luôn dưới 1.000 thao tác.
Cách làm. Đo với 2–5 người ở 500 và 6.000 card. Nếu bản lưu toàn bộ quá nặng thì chia nhỏ cách lưu. Cho mọi người có quyền sửa dọn nhật ký, không chỉ chủ board.
Task. T-collab-scale, chưa tạo; đo sau khi luồng phụ xong hẳn.
7. Máy yếu và thiết bị
Hiện trạng. Mọi số đo làm trên một máy Mac M4. Đã chạy Chrome có và không có WebGPU, Chrome không GPU, nhân Safari và Firefox bản test. Chưa thử Safari thật, Edge, Windows, iPad, điện thoại, màn 120 hình mỗi giây. RAM của trang ở 6.000 card chưa đo.
Đích. Mỗi tổ hợp hệ điều hành và trình duyệt ở một trong ba trạng thái: đạt, quay về cách vẽ cũ và chạy đúng, hoặc chưa đo kèm lý do. Board 6.000 card dùng tối đa 1 GB RAM trên máy tính, 400 MB trên iPad và điện thoại.
Cách làm. Chọn 2–3 máy thật: một laptop Windows có card đồ hoạ tích hợp đời cũ, một iPad, một điện thoại Android tầm trung. Chạy cùng một kịch bản đo cố định trên cả ba.
Task. T-device-lab, chưa tạo.
Đích và task mới ở đây là đề xuất ngày 06/11/2026, chưa chốt. Nguồn: src/collaboration/canvas-realtime-room.ts (gom 120 ms, cửa sổ 50 ms, chờ đọc 4 giây), src/lib/cloud/board-content.ts (giới hạn 1 MiB), plans/gav-289-worker-renderer.md, MR !565 và !574.
Ca đầu tiên: board 500 ảnh
3Thử kéo board
Phần 3 tới 13 kể lại ca đầu tiên đã giải xong: board 500 ảnh, khi zoom xa. Đây cũng là nơi trang giải thích các khái niệm dùng ở mọi phần sau: khung hình, trình duyệt bận, lớp. Ba con số trước và sau:
Mô phỏng dưới đây không phải board thật, nhưng chạy theo số đo thật. Board tự trượt qua lại như khi bạn cầm chuột kéo. Ở chế độ Trước, mô phỏng cố tình bỏ đúng bằng tỉ lệ đã đo trên board thật, nên bạn thấy nó khựng y như trên máy. Ở chế độ Sau, nó bỏ khung theo số đo sau khi sửa.
Cách xem: chọn và 500 ảnh, rồi bấm qua lại giữa Trước và Sau. Để ý ba ô bên dưới: số , độ bận, và dải ô đỏ. Đổi sang 50 hay 200 ảnh để thấy board càng nhiều ảnh thì càng nặng.
Mỗi viền mờ quanh ảnh là một lớp mà trình duyệt phải xếp lại trong từng khung hình.
4Số đo trước và sau
Đo thế nào: cùng một máy, cùng board 500 ảnh xếp thành lưới đều. Máy tự gửi 240 lần cuộn chuột để kéo và zoom như người thật, rồi ghi lại thời gian trình duyệt làm từng việc. Mỗi bên đo 3 lần, lấy số ở giữa (), để một lần đo xui không làm lệch kết quả.
Trang đưa số đo lên trước. Cột "Sau" là kết quả của giải pháp ở phần 13. Các phần 5 tới 11 giải thích từng con số này nghĩa là gì và vì sao nó cao như vậy.
"Trước" là tắt, tức cách vẽ cũ. "Sau" là công tắc bật, có . "" là cùng máy đó nhưng người thợ chính bị làm chậm 4 lần. Dưới mỗi chỉ số có một dòng nói con số đó nghĩa là gì.
| Đo cái gì | Trước | Sau | So sánh | Giảm | Mục tiêu |
|---|
Trước: 89808e8ed, công tắc tắt, trung vị 3 lần đo. Sau: d3f1c1163, công tắc bật, trung vị 3 lần, hai phiên đo nên ghi thành khoảng; cột Giảm tính theo số tệ hơn của khoảng. Số lớp đo trên board 750 ảnh ở 8bf9cd172. Máy Apple M4, Chrome 156, màn 1080p.
Lý thuyết
5Một giây có 60 khung hình
Màn hình máy tính thay hình 60 lần mỗi giây, giống một cuốn sách lật thật nhanh. Mỗi hình gọi là một . Để bạn thấy board trượt mượt theo tay, mỗi khung phải là một hình mới, board nằm ở vị trí mới hơn khung trước một chút.
Một giây chia cho 60 thì mỗi khung có khoảng 16,7 (mili giây, một phần nghìn giây) để trình duyệt chuẩn bị xong. Kịp thì hình mới hiện ra. Không kịp thì màn hình không chờ: nó giữ nguyên hình cũ thêm một nhịp, và khung đó coi như . Lúc hình mới cuối cùng xuất hiện, board đã nhảy một đoạn xa hơn bình thường.
Thỉnh thoảng bỏ một khung thì mắt không nhận ra. Khi hơn nửa số khung bị bỏ, board đi theo kiểu đứng, nhảy, đứng, nhảy. Đó là cái giật bạn thấy. Dải dưới đây là một giây thật trên , trước khi sửa.
Một giây kéo board 500 ảnh trên máy yếu, trước khi sửa: khoảng 39 trên 60 khung bị bỏ.
6"Trình duyệt bận 873 ms mỗi giây" nghĩa là gì
Trình duyệt chạy gần như mọi việc của một trang web trên một . Hãy hình dung đó là một người thợ duy nhất: chạy code của Gavana, tính chỗ cho từng card, , và nhận cú kéo chuột của bạn. Người thợ này chỉ làm được một việc tại một thời điểm.
Con số "bận" đếm xem trong mỗi giây (1.000 ), người thợ đó phải làm việc bao nhiêu ms. Bận 300 nghĩa là làm 300 ms, nghỉ 700 ms: thoải mái. Bận 873 nghĩa là chỉ còn 127 ms rảnh. Chia cho 60 thì mỗi khung chỉ dư khoảng 2 ms. Chỉ cần một khung nặng hơn bình thường một chút là trễ hạn.
Vì sao bận lại gây giật? Mỗi khung có hạn chót cứng 16,7 ms, giống một chuyến xe buýt chạy đúng giờ, cứ 16,7 ms một chuyến. Người thợ chưa làm xong hình mới khi xe tới thì hình đó lỡ chuyến: màn hình giữ hình cũ, khung đó . Hình mới phải chờ chuyến sau.
Thêm nữa, người thợ không nhận việc mới khi đang dở việc cũ, nên cú kéo chuột của bạn phải xếp hàng chờ. Bận gần 1.000 nghĩa là hàng chờ không bao giờ hết: board phản ứng chậm hơn tay bạn, lúc đứng lúc nhảy. Đó là cảm giác giật.
Bấm ba tình huống dưới đây để xem. Mỗi thanh xanh là thời gian người thợ làm một khung. Mỗi vạch dọc là một chuyến xe: xanh lá là kịp, đỏ là lỡ.
Dòng thời gian là mô hình đơn giản, phóng to 200 ms đầu của một giây kéo. Việc mỗi khung lấy từ số đo: 873 ms ÷ 60 khung ≈ 14,5 ms cho máy mạnh; 234 ÷ 60 ≈ 3,9 ms sau khi sửa. Với , mỗi khung cần quá một nhịp, nên mô hình đặt khoảng 40 ms để khớp với tỉ lệ đo được 65% khung bị bỏ. Số 873 là lần đo ngày 30/10 có tách thời gian theo từng bước (dùng ở phần 7). Bảng ở phần 4 ghi 908 vì đó là 3 lần của đợt đo cuối. Hai số cùng một tình huống: kéo board 500 ảnh trên máy mạnh, cách vẽ cũ.
7Mỗi khung hình đi qua năm bước
Để ra được một , trình duyệt làm năm bước theo thứ tự, như một dây chuyền. Bốn bước đầu do người thợ duy nhất () làm. Bước cuối giao cho , một con chip riêng chuyên vẽ. Bước nào chậm thì cả dây chuyền chậm theo.
Tính vị trí camera
Code của Gavana tính xem bạn đã kéo tới đâu và báo cho trình duyệt.
Luồng chínhXếp chỗ cho card
Tính kích thước và vị trí của từng hộp trên trang.
Luồng chínhGhi lệnh vẽ
Ghi ra danh sách cần vẽ gì, ở đâu. Chưa tô điểm ảnh nào.
Luồng chínhXếp lớp
Quyết định thứ gì thành riêng, lớp nào đè lớp nào.
Luồng chínhGhép và hiện
Card đồ hoạ chồng các lớp lại thành hình trên màn hình.
Card đồ hoạBiểu đồ dưới cho thấy trong một giây kéo board 500 ảnh trên máy mạnh, người thợ tiêu thời gian vào bước nào. Vạch đứt ở cuối là 1.000 , tức bận kín.
Bản sau mới đo tổng, chưa tách theo từng bước. Hai phiên đo cho 93 và 234 ms.
Đọc biểu đồ: code của Gavana (Script) chỉ tốn 108 ms. Tính chỗ gần như không tốn gì. Bước xếp lớp chiếm 633 ms, gần ba phần tư tổng thời gian bận. Nghĩa là board chậm không phải vì code của mình, cũng không phải vì tô ảnh, mà vì trình duyệt phải sắp xếp quá nhiều lớp. Muốn nhanh thì phải giảm số lớp.
Card đồ hoạ thì không phải chỗ chậm: phần việc của nó (tô điểm ảnh và chồng lớp) chỉ tốn 100–300 ms mỗi giây trên cùng máy, và không lần nào màn hình phải hiện ô trống vì card đồ hoạ vẽ không kịp. Người thợ chính mới là chỗ nghẽn.
Phần 8 và 9 tả board đang được dựng thế nào. Phần 10 giải thích vì sao lại có nhiều lớp đến vậy.
Hiện trạng
8Canvas gồm những gì
Màn hình board trông phẳng như một tờ giấy. Bên trong, trình duyệt dựng nó thành nhiều chồng lên nhau, giống những tấm phim trong suốt xếp chồng trên bàn đèn. Mỗi tấm được vẽ một lần rồi giữ lại. Sau đó chỉ việc chồng các tấm lên nhau để ra hình bạn thấy.
Chia lớp có lợi: khi bạn kéo board, trình duyệt chỉ cần trượt các tấm, không phải vẽ lại từng ảnh. Nhưng có giá: trong mỗi , trình duyệt vẫn phải kiểm lại danh sách các tấm, tấm nào nằm trên tấm nào. Mười tấm thì việc này nhanh. Hàng trăm tấm thì nó thành việc nặng nhất, như phần 7 đã cho thấy.
Board 500 ảnh lúc đứng yên chỉ có 10 lớp, chia thành bốn nhóm dưới đây. Bấm các nút để xem lúc kéo thì số lớp thay đổi ra sao.
- 1Nền và lưới chấm. Màu nền và các chấm lưới giúp bạn biết mình đang ở đâu trên board. Lưới thay đổi theo mức zoom, nên trong lúc kéo hay zoom nó được tính lại ở mỗi khung hình.
- 2Đường nối. Các sợi dây nối card với nhau, vẽ bằng SVG (một cách vẽ hình bằng đường nét). Khi bạn kéo, trình duyệt tách lớp này ra riêng để trượt cho nhanh. Chính việc tách riêng này dẫn tới vấn đề ở phần 10.
- 3Các card. Mọi card nằm chung trên một tấm nền lớn. Kéo board là trình duyệt dời cả tấm nền, không dời từng card, giống kéo cả tấm bảng ghim đầy ảnh. Nhưng mỗi card trông như một ô ảnh lại được ghép từ nhiều mảnh nhỏ mà trình duyệt phải theo dõi riêng: khung, ảnh, tên, viền, nhãn. Zoom xa, mỗi card khoảng 11 mảnh, cả board 500 ảnh là 5.747 mảnh. Zoom gần, card hiện thêm ô gõ prompt và các nút, lên khoảng 37 mảnh, cả board là 18.747 mảnh.
- 4Thanh công cụ, menu, khung chọn. Luôn nằm trên cùng và không đổi khi bạn kéo board, nên gần như không tốn gì.
Bấm "Đang kéo, trước" để xem điều gì xảy ra với lớp số 3.
Vì sao mỗi phần của card lại là một mảnh riêng?
Trình duyệt không biết "card" là gì. Nó chỉ biết các hộp, mỗi hộp có kích thước, vị trí, màu và có thể nhận cú bấm riêng. Card là do code của Gavana ghép nhiều hộp lại, giống một mô hình Lego mà trình duyệt chỉ thấy từng viên.
Mỗi phần phải là một hộp riêng vì nó làm một việc riêng: ảnh co giãn theo zoom, tên là chữ thật để sửa và chọn được, viền đổi màu khi rê chuột hay khi chọn, nút phải bấm được, ô prompt phải gõ được. Gộp hết thành một tấm hình phẳng thì không gõ, bấm hay chọn chữ được nữa.
Cái giá là khi board di chuyển, trong mỗi khung hình trình duyệt phải xem lại từng hộp: to bao nhiêu, nằm đâu, có đè lên hộp nào không. Khi zoom xa thì không ai gõ hay bấm vào card, nên lớp GPU vẽ card thành một tấm hình phẳng. Bạn rê chuột hay bấm vào card nào thì card thật với đủ các hộp quay lại ngay.
Board có sẵn một cách tiết kiệm: card nào nằm ngoài màn hình thì không dựng, giống như không bày đồ ra bàn khi chưa dùng. Khi bạn zoom gần vào một góc, chỉ vài chục card được dựng, nên board vẫn nhanh. Nhưng khi zoom xa để xem toàn board, card nào cũng nằm trong màn hình, nên cả 500 card đều phải dựng. Đó chính là lúc chậm nhất, và cũng là lúc lớp GPU ở phần 13 vào việc.
Mỗi ảnh được lưu sẵn ba bản: rộng 480 px, 1.280 px và 2.560 px. Card nhỏ trên màn hình thì dùng bản nhỏ cho nhẹ. Phóng to qua một ngưỡng thì đổi sang bản lớn hơn cho nét. Việc đổi bản này là nguyên nhân chậm thứ hai, ở phần 11; phần 17 nói đầy đủ cách chọn bản.
9Từ dữ liệu tới màn hình: React dựng board thế nào
Năm bước ở phần 7 bắt đầu khi trang đã có sẵn các hộp. Trước đó còn một bước: code của Gavana, viết bằng React, phải quyết định trên trang có những hộp nào. React giống người lập bản vẽ: nhận dữ liệu board (vị trí, ảnh, prompt của từng card) rồi viết ra danh sách hộp. Khi dữ liệu đổi, React vẽ lại (re-render) những phần bị ảnh hưởng, so với bản cũ, và chỉ sửa những hộp thật sự khác đi.
Mỗi lần vẽ lại tốn thời gian của người thợ chính. Nên câu hỏi quan trọng là: mỗi thao tác của bạn làm React vẽ lại bao nhiêu thứ. Phần này đi theo đường của một board, từ lúc mở tới lúc bạn thao tác, rồi đưa số đo.
Mở một board: bảy bước
- Trang board chỉ chạy trong trình duyệt. Server không dựng sẵn board; trong lúc tải code, bạn thấy một khung xương.
- Kiểm tra đăng nhập. Gần như mọi bước sau đều chờ bước này.
- Lấy bản có sẵn. Nếu máy đã từng mở board này, trang đọc bản lưu trên máy. Cùng lúc, trang hỏi server một bản xem trước để hiện ngay một hình tĩnh của board. Hình này chỉ để nhìn, chưa sửa được.
- Đọc board từ cơ sở dữ liệu (Firestore) nếu máy chưa có bản lưu.
- Đưa card vào bộ nhớ của trang và đặt camera: vị trí và mức zoom lần trước, hoặc vừa khung cả board.
- Nối vào phòng realtime để thấy ngay sửa đổi của người khác. Cứ 30 phút, phòng ghi một bản lưu vào Firestore.
- Tải ảnh, chỉ cho card gần màn hình. Mỗi card chọn một trong ba bản (480, 1.280 hay 2.560 px) theo kích thước của nó và mức zoom (phần 17).
React dựng ra những gì
Board gồm: nền và lưới chấm, đường nối, danh sách card, rồi lớp khung chọn và con trỏ của người cùng sửa. Mọi card nằm trên một tấm nền. Kéo board là dời tấm nền bằng một lệnh CSS duy nhất, còn mỗi card tự giữ vị trí của nó trên tấm nền.
Card nằm xa màn hình thì không dựng. Card vừa ra khỏi màn hình được giữ thêm 30 giây, tối đa 150 card, để quay lại không phải dựng lại từ đầu. Nhưng khi xem toàn board, card nào cũng nằm trong màn hình, nên cách này không bớt được gì.
Card ảnh nhỏ hơn 96 px trên màn hình chuyển sang dạng gọn: giữ ảnh, bỏ thanh thông tin, nhãn, 8 nút kéo đổi cỡ và nút hành động. Card đổi dạng tối đa 400 một lần, để zoom qua lại quanh ngưỡng không làm card nhấp nháy. Đây là lý do board 500 ảnh có khoảng 5.700 hộp khi xem toàn cảnh nhưng khoảng 18.700 hộp khi zoom gần (phần 8).
Khi bạn thao tác, React vẽ lại những gì
| Thao tác | Cái gì thay đổi | React vẽ lại gì |
|---|---|---|
| Kéo board | Chỉ vị trí camera. Mỗi , code ghi thẳng vị trí mới vào tấm nền, không đi qua trang. | Chỉ khung board, lưới và lớp . Không card nào vẽ lại. Danh sách card cần dựng chỉ tính lại khi camera đi xa hơn 15%, tối đa 10 lần mỗi giây. |
| Zoom | Vị trí và mức zoom. | Mỗi card chỉ hỏi một câu có/không: "mình có nhỏ hơn 96 px không". Chỉ card đổi câu trả lời mới vẽ lại. Khi lớp GPU đang vẽ, đổi bản ảnh đợi bạn dừng tay; ở cách vẽ cũ thì đổi ngay (phần 17). |
| Kéo một card | Vị trí của card đó và các đường nối của nó. | Trong lúc kéo, code ghi thẳng vị trí vào hộp và đường nối, React không vẽ lại. Thả tay mới lưu vị trí một lần. Sửa từ người khác tới trong lúc kéo được giữ lại, thả tay mới áp. |
| Chọn, rê chuột | Card nào đang được chọn, đang được rê. | Mỗi card nhận một giá trị đúng/sai và được React bỏ qua nếu giá trị không đổi. Chỉ card đổi trạng thái vẽ lại. Rê chuột chỉ vẽ lại 2 đường nối được tô sáng. |
| Gõ prompt | Chữ trong ô gõ. | Chữ nằm trong ô gõ; 120 ms sau khi bạn ngừng gõ mới ghi vào board, và khi đó chỉ một card có dữ liệu mới. Còn sót: vài phần nội dung riêng của card vẫn vẽ lại theo mỗi lần trang vẽ lại. |
| Ảnh mới về, người khác sửa | Dữ liệu của một hay vài card. | Sửa từ người khác được gom trong 50 ms rồi áp một lần. Card nào dữ liệu y như cũ được giữ nguyên nên không vẽ lại. Đang kéo hay đang gõ thì hoãn tới khi xong. |
React có phải nguyên nhân chậm không?
Đo trên cùng máy, board 500 ảnh, ngày 30/10, trước khi có lớp GPU. Mọi số là ms trong mỗi giây thao tác: trong 1.000 ms, người thợ chính tốn bao nhiêu ms cho từng việc. Bốn cột việc cộng lại đúng bằng cột tổng.
| Tình huống | Code (React và Gavana) | Tính chỗ (style, layout) | Xếp lớp | Việc khác | Người thợ bận tổng |
|---|---|---|---|---|---|
| Kéo, xem toàn board | 108 | 6 | 633 | 126 | 873 |
| Zoom, xem toàn board | 121 | 3 | 365 | 434 | 923 |
| Zoom quanh mức 100% | 437 | 41 | 199 | 291 | 968 |
Khi xem toàn board, React chỉ là phần nhỏ; chỗ nặng là (phần 10). Khi zoom quanh mức 100%, code tốn hơn hẳn vì card liên tục vượt ngưỡng 96 px và phải dựng thêm hoặc gỡ bớt nút và ô prompt. Đó là phần việc (phần 14) sẽ gánh.
"Việc khác" là các việc còn lại mà lần đo không tách riêng (ví dụ tô hình, xử lý chuột, dọn bộ nhớ), và khi zoom thì có cả ảnh (phần 11). Riêng việc giải mã khi zoom xem toàn board tốn 779 ms mỗi giây, nhưng con số đó cộng trên mọi luồng nên không trừ thẳng vào cột này được. Kéo ở zoom 100% chưa đo được: chuột nằm trên một card nên cú lăn không di chuyển board.
React vẫn vẽ lại nhiều hơn mức cần. Đợt đo ngày 04/11 thấy kéo một ảnh trên board 500 ảnh làm khoảng 1.000 lần vẽ lại card thừa. Bước 0.3 của GAV-288 sửa phần này. Đo trên board 200 card trộn nhiều loại. Đơn vị ở đây khác bảng trên: số lần vẽ lại cho một thao tác, không phải ms. Năm dòng đầu đếm card; dòng cuối đếm mọi thành phần trên trang (card, thanh công cụ, chú thích…), nên số lớn hơn hẳn.
| Thao tác | Trước (lần) | Sau (lần) |
|---|---|---|
| Kéo một card | 257 | 5–10 |
| Zoom ra khi xem toàn board | 251 | 6 |
| Gõ một prompt | 152 | 4 |
| Undo | 251 | 5 |
| Nối một đường | 516 | 17 |
| Kéo board ở zoom làm việc: mọi thành phần vẽ lại | 8.265 | 442 |
Nguồn: đọc code canvas-client-page.tsx, infinite-canvas.tsx, canvas-node.tsx, canvas-realtime-room.ts; số đo thời gian ở docs/research/canvas-gpu-rendering-2026-10.md; số lần vẽ lại ở plans/gav-288-gpu-overview-all-cards.md (bước 0.3) và các test đếm lần vẽ lại như canvas-node-zoom-renders.dom.test.tsx.
Phân tích
10500 tấm kính
Lúc đứng yên, board chỉ có 10 . Lúc kéo, số lớp vọt lên khoảng 519. Chuyện xảy ra như sau.
Khi bạn kéo, trình duyệt tách lớp đường nối (số 2) ra riêng để trượt cho nhanh. Nhưng các card phải nằm đè lên đường nối, để dây không vắt ngang qua ảnh. Trình duyệt có một quy tắc: thứ gì nằm trên một lớp riêng thì cũng phải thành lớp riêng, nếu không nó không giữ được thứ tự trên dưới. Thế là từng card một bị tách thành một lớp riêng.
Board 500 ảnh vì thế có khoảng 519 lớp, gần như mỗi card một lớp. Trình duyệt phải sắp xếp lại toàn bộ số lớp đó trong mỗi , 60 lần mỗi giây. Bảng dưới cho thấy số lớp tăng đúng theo số ảnh.
Mỗi ảnh là một tấm kính riêng. Trong mỗi khung hình, người thợ (chấm đỏ) phải đi qua từng tấm, kiểm vị trí và thứ tự, rồi mới đưa cho . 500 ảnh là 500 tấm, 60 lần mỗi giây.
Khi zoom xa, mọi ảnh được in sẵn lên một tấm poster do card đồ hoạ giữ. Người thợ không phải đi qua từng ảnh nữa. Kéo board chỉ là bảo card đồ hoạ trượt tấm poster đi.
| Số ảnh trên board | Lớp lúc đứng yên | Lớp lúc đang kéo |
|---|---|---|
| 50 | 10 | 61 |
| 200 | 10 | 217 |
| 500 | 10 | ≈ 519 |
Hai cách sửa rẻ đều làm tệ hơn
Trước khi làm lớp GPU, mình thử hai cách rẻ để bớt lớp, mỗi cách chỉ sửa vài dòng CSS (phần quy định cách trang hiển thị): ẩn lưới chấm nền, và không tách riêng lớp đường nối. Số lớp và thời gian có giảm như mong đợi. Nhưng khung hình khi kéo lại tăng từ 1,5% lên 13,6–15,5%, tức tệ hơn khoảng 10 lần.
Vì sao bớt lớp lại tệ hơn? Một lớp giống một bức tranh đã vẽ xong. Muốn dời nó thì chỉ việc trượt, không cần vẽ lại. Khi card không còn là lớp riêng, cả 500 ảnh bị vẽ chung lên một tấm lớn, cùng với lưới chấm và đường nối. Trong lúc kéo, lưới chấm và đường nối thay đổi theo từng khung. Trên một tấm, chỉ cần một chi tiết đổi là phải tô lại cả tấm, kéo theo cả 500 ảnh nằm trên đó, 60 lần mỗi giây. Giống một bức tranh tường: muốn sửa một nét thì phải sơn lại cả bức.
Ít tấm nên xếp nhanh. Nhưng một chi tiết nhỏ đổi (một chấm lưới, một sợi dây) là phải tô lại cả tấm chứa 500 ảnh. Tốn ở bước tô.
Không phải tô lại, chỉ trượt. Nhưng mỗi khung phải xếp lại 500 tấm. Tốn ở bước xếp lớp. Đây là cách cũ, và đo thấy nó đỡ tệ hơn cách gộp.
Với cách vẽ cũ, chỉ chọn được tốn ở chỗ này hay tốn ở chỗ kia. Lớp GPU thoát khỏi thế kẹt: chỉ có một tấm, nên không phải xếp nhiều lớp. Tấm đó cũng không bao giờ phải tô lại trên , vì ảnh đã nằm sẵn trong bộ nhớ của card đồ hoạ. Mỗi khung, card đồ hoạ tự vẽ lại 500 ảnh nhỏ từ bộ nhớ đó. Với nó đây là việc nhẹ, vì nó được làm ra để tô hàng triệu điểm ảnh cùng lúc.
11Giải mã ảnh lại mỗi lần zoom
Đây là nguyên nhân thứ hai, chỉ xảy ra khi zoom. Ảnh được lưu ở dạng nén để nhẹ. Muốn hiển thị, trình duyệt phải , tức là mở nén thành từng điểm ảnh, giống giải nén một file zip trước khi dùng. Việc này cũng do người thợ chính làm.
Như phần 8 đã nói, mỗi ảnh có ba bản. Khi bạn zoom vào, card to lên. Vượt qua ngưỡng là trình duyệt đổi sang bản lớn hơn và giải mã ảnh lại từ đầu. Một cú zoom trên board 500 ảnh có thể làm cả 500 ảnh đổi bản cùng lúc, ngay giữa chuyển động. Riêng việc này tốn khoảng 0,8 giây trong mỗi giây zoom: người thợ gần như chỉ ngồi mở file.
Cách sửa đi kèm: khi lớp GPU đang vẽ, trong lúc bạn đang zoom thì không đổi bản ảnh. Đợi bạn dừng tay rồi mới nâng lên bản nét hơn. Ở cách vẽ cũ (zoom gần, hoặc công tắc tắt) quy tắc này chưa áp; đó là việc của T-img ở phần 17. Khi bật, ảnh đã nằm sẵn trong bộ nhớ card đồ hoạ nên không phải giải mã lại. Kết quả: giải mã khi zoom ra giảm từ 1.036 xuống 0 mỗi giây.
Giải pháp
12Ba hướng đã cân nhắc
Biết chỗ chậm rồi (phần 10 và 11), có ba cách sửa. Mình cân cả ba trước khi viết code, theo bốn câu hỏi: có bỏ được chỗ chậm không, có làm hỏng việc gõ chữ, trình đọc màn hình và test không, tốn bao lâu, và rủi ro thế nào.
| A. Giữ cách cũ, vá từng chỗ | B. Thêm lớp GPU cho lúc zoom xa | C. Vẽ lại toàn bộ bằng GPU | |
|---|---|---|---|
| Là gì | Giữ nguyên cách dựng board. Thêm bản ảnh nhỏ, không đổi bản ảnh giữa lúc zoom, bớt hộp trên mỗi card. | Một tấm vẽ chỉ vẽ ảnh khi card nhỏ trên màn hình. Mọi thứ người dùng chạm vào vẫn là hộp như cũ. | Mọi loại card, đường nối, khung chọn đều do GPU vẽ. Trang chỉ còn vài hộp phụ. |
| Bỏ được chỗ chậm? | Giảm được ảnh. Bước thì chưa tìm ra cách: hai lần thử đều tệ hơn (phần 10). | Có, ngay từ cách làm: một thay cho khoảng 520, ảnh nằm sẵn trong card đồ hoạ nên không giải mã lại. | Có, như B. Khi zoom gần thì lợi thêm không nhiều, vì board 50–200 card đã mượt. |
| Gõ chữ, trình đọc màn hình, test | Không đổi | Không đổi: phần người dùng chạm vào vẫn là hộp thật. | Phải làm lại từ đầu cả ba. Figma đã phải dựng riêng một bản sao trang cho trình đọc màn hình. Khoảng 110 file test canvas mất chỗ bám. |
| Thời gian | Vài ngày mỗi chỗ vá, chưa biết bao giờ hết | Thử 2–3 ngày, làm thật khoảng 2–3 tuần | Nhiều tháng |
| Rủi ro | Thấp, nhưng có thể không lên được 500 card | Vừa phải và khoanh vùng được: nằm sau , hỏng thì quay về cách cũ | Cao: viết lại màn hình được sửa nhiều nhất |
Chọn B trước, kèm hai chỗ vá rẻ của A mà B cần sẵn: bản ảnh 128 px và không đổi bản ảnh giữa lúc zoom. Lý do: chỗ chậm nằm đúng ở lúc zoom xa, và đó cũng là lúc không ai gõ chữ hay đọc nhãn, nên cái giá của GPU gần như không phải trả. Phần 13 là B.
Sau khi B chạy, đo thêm board lẫn nhiều loại card (chữ, Section, đường nối) thì thấy B chưa đủ: các hộp còn lại vẫn giữ board ở mức khoảng 240 card. Nên mình làm tiếp một phiên bản của C nhưng giữ lại cái hay của B: GPU vẽ cả board, còn card đang sửa vẫn là hộp thật. Đó là ở phần 14.
Các app khác làm thế nào
Hai người tìm độc lập (Claude và Codex), chỉ giữ điều có nguồn gốc từ chính hãng:
- tldraw dùng hộp như Gavana, ẩn hình ngoài màn hình và xin ảnh nhỏ hơn khi zoom xa. Bản 4.4 chuyển khung chọn và viền rê chuột sang một tấm vẽ, nhanh hơn tới 25 lần trong vài tình huống. Đúng kiểu lai như hướng B.
- React Flow cũng dùng hộp. Chính tài liệu của họ nói nguyên nhân chậm chính là React vẽ lại thừa (phần 9).
- Excalidraw vẽ cả cảnh trên một tấm vẽ, khi sửa chữ thì đặt một ô gõ thật lên trên. Giữ hai thứ khớp nhau khi zoom đã gây lỗi.
- Figma vẽ hoàn toàn bằng GPU (nay dùng , dự phòng WebGL). Cái giá: mất hết hỗ trợ trình đọc màn hình của trình duyệt, phải dựng lại một bản sao trang riêng.
- Google Docs chuyển sang vẽ bằng tấm vẽ vì tốc độ, và báo trước là tiện ích đọc trang sẽ hỏng.
Bài học chung: app dùng hộp thì lớn lên bằng cách ẩn bớt, dùng ảnh nhỏ, rồi chuyển đúng phần nặng nhất sang tấm vẽ. App vẽ toàn bộ bằng GPU thì phải làm lại việc gõ chữ và trình đọc màn hình.
13Một lớp GPU cho lúc zoom xa
Có một cách sửa triệt để: viết lại toàn bộ board để vẽ hết. Cách đó mất hàng tháng. Nó còn làm hỏng những thứ cần chữ và hộp thật trong trang: gõ prompt, chọn chữ để copy, phần mềm đọc màn hình cho người khiếm thị, và các bài test tự động.
Mình chọn cách nhỏ hơn: thêm một vẽ bằng GPU, và lớp này chỉ làm việc khi zoom xa. Đó đúng là lúc board chậm nhất, vì 500 card cùng hiện. Đó cũng là lúc không ai gõ hay đọc chữ trên card, vì card quá nhỏ. Zoom gần lại thì card thật quay về như cũ.
In ảnh nhỏ thành từng trang
Mỗi ảnh được thu nhỏ về 128 px, đủ nét khi card nhỏ trên màn hình. Rồi ảnh được xếp vào các "trang" trong bộ nhớ card đồ hoạ, 64 ảnh một trang, giống dán ảnh vào album. 500 ảnh vừa 8 trang, tốn 44,7 MB bộ nhớ card đồ hoạ, dưới mức trần 64 MB đặt cho tính năng này.
8 trang × 64 = 512 ô cho 500 ảnh. 12 ô trống ở trang cuối.
Trả lại card thật khi bạn chạm vào
- Rê chuột, chọn, focus hay sửa một ảnh: card thật với đủ nút và chữ quay lại ngay trong cùng , nên bạn không thấy chỗ chuyển.
- Card đã được GPU vẽ thì ẩn khỏi trang, nhưng vẫn bấm được, và phần mềm đọc màn hình vẫn đọc được tên.
- Nếu card đồ hoạ mất bộ nhớ đột ngột, board quay về cách vẽ cũ ngay, rồi in lại các trang khi GPU sẵn sàng. Board không bị trống lúc chuyển.
- Đang zoom thì không đổi bản ảnh (phần 11).
Vì sao thu ảnh về 128 px
Card đồ hoạ có bộ nhớ riêng, và trang web không hỏi được nó còn bao nhiêu, nên phải tự đặt giới hạn. Ảnh trong bộ nhớ card đồ hoạ không nén: mỗi điểm ảnh 4 byte. 500 ảnh ở bản 480 px tốn khoảng 440 MB, quá nhiều cho một máy thường. Ở 128 px chỉ còn khoảng 31 MB, ở 256 px khoảng 125 MB. Khi zoom xa card chỉ rộng vài chục điểm ảnh, nên 128 px vẫn đủ nét.
Rủi ro mới mà GPU mang theo
- Card đồ hoạ có thể mất bộ nhớ bất kỳ lúc nào (đổi card, máy ngủ dậy, driver lỗi). Mọi ảnh trong đó mất theo. Board phải tự quay về cách cũ rồi nạp lại.
- Ảnh từ máy chủ khác phải được cho phép (CORS) mới đưa vào card đồ hoạ được. Link ảnh có chữ ký vẫn chưa đủ.
- Màu phải khớp: ảnh do GPU vẽ và ảnh thật trong card phải trông giống hệt, nếu không lúc đổi qua lại sẽ thấy nháy màu.
- Trình đọc màn hình không thấy điểm ảnh trên tấm vẽ, nên card thật vẫn phải nằm trong trang, chỉ ẩn đi.
Card đã vẽ rời khỏi việc xếp lớp
Sau khi lớp GPU chạy, đo lại trên board 750 ảnh thì thấy một chỗ sót. Card đã được GPU vẽ thay và phần bên trong đã ẩn, nhưng cái vỏ ngoài của card vẫn bị trình duyệt tính là một lớp riêng. Nghĩa là người thợ vẫn phải xếp hàng trăm lớp không ai nhìn thấy.
Ẩn hẳn cả vỏ card thì số lớp giảm từ 335 xuống 19, thời gian từ 429–640 xuống 8–22 mỗi giây.
14Thuê thêm một người thợ
Lớp ở trên chỉ chạy khi zoom xa. Zoom gần lại thì board quay về cách vẽ cũ, và người thợ chính vẫn gánh mọi việc. Board càng lớn, hàng nghìn card, thì chỗ đó càng nặng.
GAV-289 đi xa hơn: thuê thêm một người thợ thứ hai, gọi là (Web Worker). Người thợ này chạy song song với người thợ chính, cầm card đồ hoạ, và vẽ cả board ở mọi mức zoom. Người thợ chính chỉ còn lo phần bạn đang gõ hay sửa, nên luôn rảnh để nhận cú kéo chuột của bạn.
Vì sao cần thêm luồng phụ
Đo trên , board bắt đầu giật ở khoảng bao nhiêu card:
- Board toàn ảnh: khoảng 610 card trên máy mạnh, chỉ khoảng 180 trên máy yếu. Có GPU: khoảng 880 và 535.
- Board lẫn chữ, Section, đường nối: khoảng 200 card trên máy mạnh, khoảng 60 trên máy yếu. Có lớp GPU vẫn chỉ khoảng 240 và 60, vì các hộp còn lại vẫn nặng.
Có hai cái lợi tách riêng được. Thứ nhất, vẽ mọi card trên một tấm vẽ thì bỏ được việc và tính chỗ cho từng hộp; GPU chạy trên cũng làm được điều này. Thứ hai, chuyển cả việc vẽ sang luồng phụ thì người thợ chính không còn gánh việc vẽ board: chọn card nào cần vẽ, nạp ảnh, dựng chữ, gửi lệnh cho card đồ hoạ. Chỉ cái lợi thứ hai giữ được board mượt khi người thợ chính bị chậm hay bận chạy React.
Biểu đồ dưới so cùng một cách vẽ GPU, chạy trên luồng chính và chạy ở luồng phụ, trên một board rất lớn: 6.251 card, máy yếu.
Board 6.251 card, máy yếu
Thời gian luồng chính bận () trong mỗi giây kéo hoặc zoom. Ở bản luồng phụ, người thợ chính gần như rảnh hẳn, và không quá 2,1%, trên board lớn gấp 12 lần board 500 ảnh.
Zoom 0,85, ảnh nét nhất, máy yếu, WebGL2
Trang trước đây chỉ nêu các ô đạt. Có một ô không đạt: board 6.251 card ở zoom 0,85, mọi ảnh dùng bản nét nhất, máy yếu, luồng phụ chạy bằng WebGL2. Màn hình bỏ 19,3% khung, luồng phụ bận 350 ms mỗi giây. Làm chậm cả luồng phụ thì bỏ tới 59,4%, luồng phụ bận 759 ms/s. Cùng ô đó với WebGPU thì đạt: bỏ 2,1%.
Nguyên nhân: mỗi khung, luồng phụ đẩy quá nhiều ảnh nét vào card đồ hoạ cùng lúc. Hướng sửa (bước 7, đề xuất): giới hạn số MB ảnh nạp mỗi khung, nạp bản nhỏ trước rồi bản nét sau, và bỏ bớt ảnh ngoài vùng nhìn thấy khi chạm trần bộ nhớ.
Những phần khó và cách xử lý
- Gõ chữ, kể cả tiếng Việt
- Khi bạn mở một card để sửa, luồng phụ ngừng vẽ card đó, và luồng chính đặt card thật (hộp React như hôm nay) đúng vị trí đó. Gõ, con trỏ, bôi chữ, bộ gõ Telex/VNI, kiểm tra chính tả, copy/paste đều là của trình duyệt như cũ. Đã thử gõ "Việt Nam — ưu đãi đặc biệt" qua bộ gõ: dấu chồng hiện đúng cả lúc gõ lẫn sau khi luồng phụ vẽ lại.
- Chữ trên card
- Luồng phụ nhờ trình duyệt dựng chữ thành ảnh rồi đưa cho card đồ hoạ, để dấu tiếng Việt chồng đúng. Cách dựng chữ bằng đường viền toán học (SDF) bị loại vì chữ nhỏ trên màn thường thì nhoè dấu. Bài học từ bản thử: chữ trong ô sửa và chữ do luồng phụ vẽ phải xuống dòng y hệt nhau, nếu không card nhảy chữ khi bạn sửa xong.
- Trình đọc màn hình
- Một danh sách ẩn, mỗi card một mục theo thứ tự đọc, có tên ảnh, nội dung chữ hay tên Section. Dùng phím mũi tên đi giữa các mục; chọn một mục thì board chọn card đó và đưa camera tới. Danh sách này không di chuyển theo camera nên kéo board không tốn gì. Các test hiện có sẽ tìm card qua danh sách này.
- Bấm vào đâu
- Luồng chính tự biết chuột đang ở trên card nào, bằng một lưới tra cứu dựng từ vị trí card (6.251 card dựng trong vài ms). Không bao giờ phải hỏi luồng phụ, nên bấm, chọn, kéo nhanh như hôm nay.
- Card đồ hoạ mất bộ nhớ, luồng phụ chết
- Dựng lại tấm vẽ và nạp lại ảnh từ bộ nhớ đệm. Không hồi lại được thì tạo luồng phụ mới. Hỏng hai lần trong một phiên thì quay về cách vẽ cũ.
- Máy và trình duyệt
- Mỗi lần mở board, trang tự kiểm tra: có thì dùng, không thì WebGL2. Không có cả hai, chỉ có GPU giả bằng phần mềm, hoặc vẽ ra sai thì dùng cách cũ, kèm lý do.
Kế hoạch bảy bước
| Bước | Làm gì | Xong khi | Ước tính | Trạng thái 06/11 |
|---|---|---|---|---|
| 1 | Tự chọn cách vẽ theo máy, luồng phụ với WebGPU và WebGL2, giới hạn nạp ảnh mỗi khung; trước hết cho board chỉ xem (trang Admin, board nhúng) | Hai trang đó mượt trên 6.251 card ở máy yếu; Safari và Firefox chạy được | 1 tuần | Live 1c04e02fc, một tài khoản |
| 2 | Board chỉnh sửa: rê chuột, chọn, khung chọn, kéo, đường nối, menu chuột phải | Mọi thao tác chạy mà không cần hộp cho từng card | 1–2 tuần | Đã merge 1db5ff817, chờ lên production |
| 3 | Card đang sửa là hộp thật; chữ xuống dòng giống nhau; gõ tiếng Việt | Không giật khi bắt đầu hay kết thúc sửa; bộ gõ đúng trên Chrome, Safari, Firefox | 1–2 tuần | Đang làm |
| 4 | Danh sách cho trình đọc màn hình, chuyển test sang đó | Test canvas xanh; VoiceOver tới và thao tác được mọi card | 1 tuần | Chờ bước 2 và 3 |
| 5 | Các loại card còn lại (video, đang tạo, List, style), xử lý mất bộ nhớ card đồ hoạ | Ép mất bộ nhớ trên Chrome và Safari vẫn hồi lại; vẽ đủ mọi loại | 1 tuần | Gần xong lệch điểm ảnh ≤ 1,03%, riêng Style 5,87% |
| 6 | Bật cho tài khoản đầu tiên trên production | Đi thử trên app.gavana.ai; mọi tổ hợp máy và trình duyệt đã đo hoặc ghi rõ chưa đo | 1 ngày | Chờ |
| 7 | Giới hạn số MB ảnh nạp vào card đồ hoạ mỗi khung; bản nhỏ trước, bản nét sau; bỏ ảnh ngoài vùng nhìn khi chạm trần | Ô zoom 0,85 ảnh nét nhất ở phần 14 bỏ ≤ 2% trên WebGL2 | Chưa ước | Đề xuất, chưa tạo |
15Đọc số đo cho đúng
Trang này dùng ba loại số. Mỗi loại nói một điều khác nhau, và đọc nhầm thì dễ kết luận sai.
Trung bình và việc dài nhất
"Bận 300 ms mỗi giây" là số trung bình. Nếu 300 ms đó chia đều cho 60 khung, mỗi khung chỉ tốn 5 ms: thoải mái. Nhưng nếu nó dồn thành một cục 50 ms, người thợ chính bận liền ba chuyến xe buýt (phần 6), ba khung liền bị bỏ, và mắt thấy một cú khựng rõ dù trung bình vẫn đẹp. Vì vậy đích có cả hai: bận ≤ 300 ms mỗi giây, và không việc nào dài quá 50 ms.
Hai kiểu khung bị bỏ
Khi có luồng phụ, một khung có thể bị bỏ ở hai chỗ. Luồng phụ bỏ: code của mình chưa vẽ xong khung đó, mình sửa được. Màn hình bỏ: luồng phụ đã vẽ xong, nhưng trình duyệt không kịp đưa lên màn hình. Trên máy yếu thật, một phần của loại thứ hai do chính trình duyệt và card đồ hoạ, code không gỡ hết được. Ở ô fail của phần 14, màn hình bỏ 19,3% còn luồng phụ chỉ bỏ 11,7%: phần chênh là của trình duyệt.
Vì sao đích là 2%, không phải 0%
Màn hình thỉnh thoảng bỏ một khung vì lý do ngoài trang: một app khác chạy nền, máy hạ xung để mát. Một khung trong năm mươi thì mắt không nhận ra. Đặt đích 0% thì không phép đo nào đạt được ổn định, và người ta sẽ thôi tin vào đích.
Màn 120 hình mỗi giây chỉ cho 8,3 ms mỗi khung. Mọi số đo ở trang này làm ở 60 hình; 120 hình chưa đo. Nguồn: các cột display, worker và page dropped trong _docs/reports/gav-289-spike/sweep/tables.md.
16Board 6.000 card: còn gì chưa đo
Luồng phụ giải quyết phần vẽ. Một board 6.000 card còn phải mở được, lưu được, cùng sửa được và vừa bộ nhớ. Các phần đó chưa ai đo. Bạn đã chốt: đo chúng sau khi luồng phụ xong hẳn, trên app thật.
| Phần | Đã biết | Chưa biết |
|---|---|---|
| Lưu board | Nội dung board (mọi card và đường nối) nằm chung trong một bản ghi của Firestore. Firestore giới hạn mỗi bản ghi 1 MiB; code cảnh báo từ 800 KB. Production đang để nội dung ngay trong bản ghi của project. | Board 6.000 card nặng bao nhiêu KB. Nếu vượt 1 MiB thì phải đổi cách lưu: đây là rủi ro lớn nhất. |
| Mở board | Đọc board, vào phòng realtime, tải ảnh theo vùng nhìn. | Mất bao lâu ở 500 và 6.000 card. |
| Cùng sửa | Định kỳ ghi bản lưu chứa toàn bộ board. | Ghi bản lưu 6.000 card tốn bao lâu, có làm đứng màn hình không; vào phòng có mất card không. |
| Bộ nhớ | Lớp GPU tự giới hạn 64 MB ảnh. Trang giữ card vừa ra khỏi màn hình thêm 30 giây, tối đa 150 card. | Luồng phụ ở 6.000 card dùng bao nhiêu bộ nhớ card đồ hoạ; cả trang dùng bao nhiêu RAM. |
| Loại card | Ảnh, chữ, video, card đang tạo, List, Style đã có bước của luồng phụ (phần 14). | Section chưa thuộc bước nào. Mỗi card đang tạo dựng một bộ vẽ đồ hoạ riêng; trình duyệt chỉ cho khoảng 16 bộ cùng lúc, nên 20 card cùng tạo có thể làm mất hiệu ứng ở vài card. |
| Phần vẫn là hộp | Card đang sửa, thanh công cụ, chọn nhiều card vẫn dùng hộp thật. | Chọn 1.000 card một lúc thì tốn bao nhiêu. |
Nguồn: src/lib/cloud/board-content.ts (giới hạn 1 MiB, cảnh báo 800 KB), /api/release của production ngày 06/11 (boardContentMode: inline), src/lib/canvas-gpu-overview/texture-budget.ts (64 MB), src/lib/canvas-viewport-loading.ts (30 giây, 150 card), src/app/(user)/canvas/components/generation-shader.tsx (một bộ vẽ cho mỗi card đang tạo).
Ảnh
17Ảnh được tải và đổi cỡ khi nào
Mỗi ảnh có ba bản: rộng 480, 1.280 và 2.560 px. Ảnh gốc chỉ được tải khi thật cần: tải xuống, xuất file, cắt ảnh, gửi cho AI làm ảnh tham chiếu, hoặc zoom rất gần.
Chọn bản nào. Lấy bề ngang ảnh trên màn hình × độ nét màn hình (màn retina là 2), cộng thêm 30% dư để zoom thêm một chút không phải tải lại. Bản nhỏ nhất vẫn đủ con số đó được chọn. Nếu không bản nào đủ, dùng bản lớn nhất.
Chỉ đổi lên. Zoom ra, ảnh không bao giờ đổi xuống bản nhỏ hơn, để zoom ra vào liên tục không làm ảnh đổi qua lại mãi. Ảnh mới cùng bản với ảnh đang hiện thì không thay.
Tải theo thứ tự. Tối đa 4 ảnh được tải và cùng lúc: card đang chọn trước, rồi card giữa màn hình, rìa màn hình, rồi vùng chờ phía trước hướng bạn đang kéo (kéo càng nhanh, vùng chờ càng xa). Card vừa ra khỏi màn hình được giữ thêm 30 giây.
Khi lớp GPU đang vẽ (zoom xa, công tắc bật), mỗi ảnh còn được thu về 256 hoặc 128 px để giữ trong card đồ hoạ, và mọi lần đổi bản được hoãn tới lúc bạn dừng tay.
Thử dưới đây: kéo thanh zoom và xem card rộng 400 px chọn bản nào.
Khi nào ảnh thay nhiều nhất
- Zoom từ xa vào trên board dày ảnh: hàng trăm card cùng vượt ngưỡng một lúc. Ở cách vẽ cũ, chúng đổi bản ngay giữa cú zoom.
- Kéo nhanh qua vùng chưa xem: card mới vào màn hình liên tục, mỗi card một lần tải.
- Mở board: mọi card trên màn hình cần ảnh ngay.
Cách cho ảnh thay ít mà vẫn mượt (đề xuất T-img)
- Không nâng bản khi đang chuyển động, ở cả cách vẽ cũ lẫn mới. Trong lúc zoom, card phóng to tạm ảnh đang có, chỉ hơi mờ; dừng tay mới tải bản nét. Các bản ở mức zoom trung gian sẽ không bao giờ được tải.
- Chênh đủ mới nâng. Chỉ nâng khi bản hiện tại thiếu khá rõ so với nhu cầu, để zoom loanh quanh một mức không làm ảnh đổi tới đổi lui.
- Giải mã xong mới thay. Ảnh mới được mở nén ở chỗ khác; ảnh cũ ở yên tới lúc đó, nên không có khung trống, không thấy giật.
- Giới hạn số ảnh đổi mỗi khung, card giữa màn hình đổi trước. Ảnh nét dần trong nửa giây thì mắt có thấy nhưng không khó chịu; một khung bị trễ thì thấy ngay.
Các con số cho bốn quy tắc trên (bao lâu thì coi là đã dừng tay, chênh bao nhiêu mới nâng, có nên mờ dần khi thay) đang được hai người tìm độc lập, Claude và Codex, từ nguồn chính hãng. Kết quả sơ bộ: không có ngưỡng thời gian nào phân biệt chắc chắn "đang nghỉ giữa cú kéo" với "đã dừng hẳn", nên phải kết hợp thêm ngón tay còn chạm hay không; OpenSeadragon giữ ảnh cũ tới khi ảnh mới giải mã xong rồi thay một lần, không mờ dần; tldraw hoãn đổi độ nét trong lúc zoom; theo Nielsen, trễ 0,2–1 giây là người dùng có nhận ra.
Gavana hiện không ghi lại người dùng thật kéo và zoom thế nào, nên các con số sẽ dựa trên nghiên cứu và các app tương tự trước. Đề xuất: ghi lại cử chỉ, ẩn danh, để kiểm lại sau khi bật.
Nguồn: src/lib/cloud/r2.ts (ba bản 480/1.280/2.560), src/lib/canvas-image-variants.ts (dư 30%, chỉ đổi lên, khi nào được tải ảnh gốc), src/lib/canvas-viewport-loading.ts (4 ảnh cùng lúc, giữ 30 giây), src/app/(user)/canvas/[id]/canvas-client-page.tsx (hoãn đổi bản chỉ khi lớp GPU đang vẽ), src/lib/canvas-gpu-overview/texture-budget.ts (256/128 px, 64 MB). Mô phỏng coi ảnh vuông, không cắt.
Lộ trình
18Đã làm tới đâu, làm gì tiếp
Đã xong
- Lớp khi zoom xa (GAV-265) và bỏ vỏ card khỏi việc (GAV-288) đã vào
main, nằm sau đang tắt, nên người dùng chưa thấy gì khác. - Vệt sọc dọc khi zoom bằng Ctrl (GAV-281) đã sửa.
- bước 1 (GAV-289) đã lên production (
1c04e02fc): board chỉ xem được vẽ bằng luồng phụ, mới bật cho một tài khoản. - Luồng phụ bước 2 (board sửa được,
1db5ff817) và GAV-309 (rê chuột không làm mờ ảnh, MR !574) đã merge, đang chờ lên production.
Thứ tự làm tiếp
Mỗi ô là một việc; mũi tên là "phải xong trước". Việc ở cùng một hàng làm song song được.
Hiện mọi số đo nằm rải rác trong các báo cáo, CI không kiểm hiệu năng, và production chỉ ghi lỗi trình duyệt chứ không ghi độ mượt. Hai việc đề xuất: T-perf-budget, mỗi thay đổi đụng tới canvas chạy một kịch bản đo cố định (50, 500, 6.000 card), vượt ngân sách thì báo đỏ; ban đầu không chặn merge vì runner CI đang chập chờn. T-rum, production gửi số mẫu ẩn danh về độ mượt, gom theo cỡ board và loại máy.
Luồng phụ chạy được trên trình duyệt nào
| Chrome, có | Đạt |
| Chrome, chỉ có WebGL2 | Đạt trừ ô zoom 0,85 ảnh nét nhất |
| Firefox (bản test) | Đạt |
| Máy không có GPU | Về cách vẽ cũ |
| khi zoom vào (bỏ 73–87% khung) | Chưa đạt |
| Safari thật, Edge, Windows, Linux, iPad, điện thoại, màn 120 hình | Chưa đo |
"Về cách vẽ cũ" nghĩa là máy đó vẫn dùng board như hôm nay, chỉ không nhanh hơn. "Chưa đạt" là đã thử và còn giật, phải sửa trước khi bật. "Chưa đo" là chưa thử. Nguồn: _docs/reports/gav-289-matrix/table.md.
19Rủi ro và chưa biết
Xếp theo mức độ, ngày 06/11/2026. Mỗi mục có lý do quan trọng và bước tiếp.
1. Cách lưu board rất lớn
Nội dung board nằm chung trong một bản ghi Firestore, giới hạn 1 MiB. Chưa biết board 6.000 card nặng bao nhiêu.
Vì sao quan trọng. Nếu vượt, phải đổi cách lưu dữ liệu: việc lớn nhất và khó quay lại nhất trong cả kế hoạch.
Bước tiếp. Đo kích thước bản ghi của board 6.000 card, trước mọi việc khác của T-open.
2. Cùng sửa ở quy mô lớn
Chưa có một số đo nào ở 500 hay 6.000 card với nhiều người. Đã từng có board mất card lúc vào phòng.
Vì sao quan trọng. Vẽ mượt mà vào phòng chậm, hoặc mất card, thì board vẫn không dùng được.
Bước tiếp. T-collab-scale, sau khi luồng phụ xong hẳn.
3. Máy thật
Mọi số đo trên một Mac M4. "Máy yếu" chỉ làm chậm người thợ chính, không làm chậm card đồ hoạ. Nhân Safari còn bỏ 73–87% khung khi zoom vào.
Vì sao quan trọng. Laptop yếu thật thường yếu cả card đồ hoạ, mà cách vẽ mới dựa vào card đồ hoạ.
Bước tiếp. T-device-lab; Safari chưa sửa xong thì Safari dùng cách vẽ cũ.
4. Ô zoom 0,85 ảnh nét nhất
Board 6.251 card trên WebGL2 bỏ 19–59% khung (phần 14).
Vì sao quan trọng. Đây là ô duy nhất của luồng phụ còn fail, và WebGL2 là đường của mọi máy không có WebGPU.
Bước tiếp. Thiết kế bước 7.
5. Ảnh mờ khi rê chuột trong luồng phụ
GAV-309 sửa xong cho lớp GPU. Luồng phụ có thể gặp lại đúng lỗi đó khi một card nổi lên trên tấm vẽ.
Vì sao quan trọng. Ảnh mờ là thứ người dùng thấy ngay.
Bước tiếp. Đang đo trong bước 3.
6. Board đo là lưới đều
Board đo toàn ảnh, xếp ô vuông đều. Board thật trộn ảnh, video, List, chữ và đường nối, nằm lộn xộn.
Vì sao quan trọng. Video và List vẽ khác; nhiều đường nối hơn thì nặng hơn.
Bước tiếp. Đo trên vài board thật của team trong T-6k.
7. Trình đọc màn hình
Card do GPU vẽ vẫn giữ tên trong trang. Chưa ai bật VoiceOver lên nghe thử.
Vì sao quan trọng. Người khiếm thị dựa hoàn toàn vào phần này.
Bước tiếp. Bước 4 của luồng phụ, nghe thử trước khi bật cho người dùng.
8. CI chập chờn
Nhiều runner hỏng và test timeout làm chậm mọi lần merge.
Vì sao quan trọng. Nó che mất lúc hiệu năng tụt thật, và làm kiểm hiệu năng tự động bị nhiễu.
Bước tiếp. T-perf-budget chạy không chặn cho tới khi runner ổn.
20Nguồn
Mọi con số trên trang lấy từ repo gavana.ai, code ở main commit eaff12526 (06/11/2026), và các báo cáo đo dưới đây.
- Số đo board 500 ảnh:
docs/research/canvas-gpu-rendering-2026-10.md,_docs/reports/gav-265-measure-final/REPORT.md; trước ở build89808e8ed, sau ởd3f1c1163. - Bỏ vỏ card, số lần vẽ lại:
plans/gav-288-gpu-overview-all-cards.md. - Board 6.251 card, ô fail:
_docs/reports/gav-289-spike/sweep/tables.md(các dòngn5000-worker-webgl2-z85-amp1-*). - Trình duyệt:
_docs/reports/gav-289-matrix/table.md; kế hoạch luồng phụ:plans/gav-289-worker-renderer.md. - Ảnh:
src/lib/cloud/r2.ts,src/lib/canvas-image-variants.ts,src/lib/canvas-viewport-loading.ts,src/lib/canvas-gpu-overview/texture-budget.ts,src/app/(user)/canvas/[id]/canvas-client-page.tsx. - Lưu và cùng sửa:
src/lib/cloud/board-content.ts,src/collaboration/canvas-realtime-room.ts,src/collaboration/canvas-checkpoint.ts. - Rê chuột: MR !565 (GAV-303), MR !574 (GAV-309, merge
eaff12526).
Bản trước của trang ghi mỗi ảnh có hai bản 480 và 1.024 px. Code chưa từng có bản 1.024; bản giữa là 1.280 px. Đã sửa ở phần 8, 9, 11 và 17.