Board 500 ảnh đã hết giật khi kéo
Khi một board có hàng trăm ảnh, kéo hay zoom bị khựng, nhất là trên . Trang này viết cho người không làm kỹ thuật và kể lại từ đầu: trình duyệt dựng board từ những gì, vì sao chỗ đó làm chậm, mình đã sửa bằng cách nào, và số đo trước, sau. 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ỗ.
Số đo
1Thử kéo board
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.
2Số đ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 11. Các phần 3 tới 9 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
3Mộ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ỏ.
4"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 5). Bảng ở phần 2 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ũ.
5Mỗ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 6 và 7 tả board đang được dựng thế nào. Phần 8 giải thích vì sao lại có nhiều lớp đến vậy.
Hiện trạng
6Canvas 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 5 đã 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 8.
- 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 11 vào việc.
Mỗi ảnh được lưu sẵn hai bản: bản nhỏ 480 px và bản lớn 1024 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 cho nét. Việc đổi bản này là nguyên nhân chậm thứ hai, ở phần 9.
7Từ dữ liệu tới màn hình: React dựng board thế nào
Năm bước ở phần 5 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 bản 480 px hay 1024 px theo kích thước của nó và mức zoom.
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 6).
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. Đổi bản ảnh đợi bạn dừng tay. |
| 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, đơn vị ms trong mỗi giây thao tác. Cột "Code (React và Gavana)" là thời gian chạy code, gồm cả React vẽ lại.
| Tình huống | Code (React và Gavana) | Xếp lớp | Người thợ bận tổng |
|---|---|---|---|
| Kéo, xem toàn board | 108 | 633 | 873 |
| Zoom, xem toàn board | 121 | 365 | 923 |
| Kéo ở zoom 100% | 437 | 199 | 968 |
Khi xem toàn board, React chỉ là phần nhỏ; chỗ nặng là (phần 8). Ở zoom 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 12) sẽ gánh.
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, số lần card vẽ lại cho mỗi thao tác:
| Thao tác | Trước | Sau |
|---|---|---|
| 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 |
| Mọi thứ vẽ lại khi kéo board ở zoom làm việc | 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
8500 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.
9Giả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 6 đã nói, mỗi ảnh có hai bản 480 px và 1024 px. Khi bạn zoom, card to lên hay nhỏ đi. Vượt qua ngưỡng là trình duyệt đổi sang bản kia và giải mã ảnh lại từ đầu. Zoom qua lại quanh ngưỡng thì 500 ảnh bị đổi bản rồi đổi lại liên tục. 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: 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. 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
10Ba hướng đã cân nhắc
Biết chỗ chậm rồi (phần 8 và 9), 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 8). | 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 11 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 12.
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 7).
- 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.
11Mộ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 9).
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.
12Thuê 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.
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 sáu bước
| Bước | Làm gì | Xong khi | Ước tính |
|---|---|---|---|
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
Tiến độ
13Tiến độ: đã làm tới đâu
Phần này để cuối: những gì đã lên code, những gì đang làm, và những gì phải xong trước khi bật cho mọi người.
Đã xong
- Lớp khi zoom xa (GAV-265) và bỏ vỏ card khỏi việc (GAV-288) đã merge vào
main. Cả hai nằm sau , công tắc đ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): card kính có hiệu ứng làm mờ nền; khi zoom, trình duyệt giữ lại một mảnh hình cũ thành vệt sọc cạnh card. Hiệu ứng mờ giờ nằm trên một riêng dưới card nên không còn vệt.
- , bước 1/6 (GAV-289, !570) merge sáng 06/11: board chỉ xem (trang Admin, board nhúng) được vẽ bằng luồng phụ. Mới bật cho một tài khoản test.
Đang làm
Luồng phụ được chia thành 6 bước, mỗi bước là một merge request riêng, merge được và tắt được độc lập với nhau. Bước 1 đã xong (ở trên). Bước 2 tới 6 đang làm song song: đưa luồng phụ vào board chỉnh sửa, hỗ trợ đủ mọi loại card, rồi bật dần cho người dùng. Bảng dưới là các trình duyệt đã thử với luồng phụ.
Chạy được trên trình duyệt nào
| Chrome, có | Đạt |
| Chrome, chỉ có WebGL2 | Đạt |
| Firefox | Đạt |
| Máy không có GPU | Về cách vẽ cũ |
| Nhân Safari khi zoom vào (bỏ 73–87% khung) | Chưa đạt |
| Safari thật, Edge, Windows, Linux, điện thoại | 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ử.
Cần xong trước khi bật cho mọi người
Năm việc dưới đây cần xong trước khi bật cho mọi người. Mỗi việc ghi rõ: hiện trạng, vì sao quan trọng, và bước tiếp theo.
Chưa bật cho người dùng
Lớp GPU đã vào main, tức bản code chính đang chạy trên production, nhưng công tắc đang tắt. Người dùng chưa thấy gì khác: board vẫn vẽ theo cách cũ.
Vì sao quan trọng. Mọi số đo ở trang này làm trên board thử. Bật cho mọi người khi chưa đo trên board thật thì có thể gặp lỗi mà board thử không có.
Bước tiếp. Bật cho tài khoản nội bộ trước, đo trên vài board thật của team, rồi mới bật dần cho người dùng.
Mới đo trên một máy
Mọi số đo làm trên một máy Mac M4. "" là chính máy đó bị làm chậm người thợ chính 4 lần bằng công cụ của Chrome.
Vì sao quan trọng. Một laptop yếu thật thường yếu cả card đồ hoạ, trong khi máy đo vẫn có card đồ hoạ mạnh. Lớp GPU dựa vào card đồ hoạ, nên trên máy yếu thật, kết quả có thể kém hơn số ở đây.
Bước tiếp. Đo lại trên một laptop Windows phổ thông và một máy Mac đời cũ.
Safari khi zoom vào
Ở bản luồng phụ (phần 12), nhân của Safari còn bỏ 73–87% khi zoom gần vào. Nghĩa là trên Safari, zoom vào còn giật hơn cả cách cũ.
Vì sao quan trọng. Một phần người dùng Mac dùng Safari. Bật luồng phụ mà chưa sửa chỗ này thì những người đó thấy tệ hơn.
Bước tiếp. Thử trên Safari thật (bản đo hiện là nhân Safari dùng để test), tìm nguyên nhân. Chưa sửa xong thì Safari dùng cách vẽ cũ.
Board đo là lưới đều
Board đo gồm toàn ảnh, xếp ô vuông đều nhau. Board thật của người dùng trộn ảnh, video, List, ô chữ và đường nối, nằm lộn xộn và đè lên nhau.
Vì sao quan trọng. Video và List có cách vẽ riêng. Đường nối nhiều hơn thì lớp số 2 nặng hơn. Số đo trên board thật có thể khác số ở đây.
Bước tiếp. Đo trên vài board thật của team, cùng cách đo như phần 2.
Trình đọc màn hình
Card do GPU vẽ vẫn giữ tên trong trang, để phần mềm đọc màn hình (người khiếm thị dùng để nghe nội dung trang) đọc được.
Vì sao quan trọng. Đây mới là cách làm theo lý thuyết. Chưa ai bật VoiceOver của Mac lên và nghe thử xem có đọc đúng, đủ, đúng thứ tự không.
Bước tiếp. Nghe thử bằng VoiceOver trên một board có lớp GPU bật, trước khi bật cho người dùng.
Nguồn: docs/research/canvas-gpu-rendering-2026-10.md, _docs/reports/gav-265-measure-final/REPORT.md, plans/gav-288-gpu-overview-all-cards.md, _docs/reports/gav-289-matrix/table.md trong repo gavana.ai.