Học từ Facebook #27: Một vòng rAF chung và solver cubic-bezier tự viết
Engine Keyframes của Facebook chạy mọi animation trên một vòng requestAnimationFrame, kẹp delta khi tab quay lại và tự giải cubic-bezier. Kèm hai lỗi và bản sửa.
Phần lớn animation trên web nên để CSS lo. Nhưng có những thứ CSS không làm được: animation vẽ trên <canvas> (sticker, reaction động), tiến trình phụ thuộc dữ liệu, hay animation phải dừng/tua theo logic JS. Khi đó ta tự gọi requestAnimationFrame (rAF), và rất dễ gặp:
- Mỗi animation một vòng rAF riêng: 50 sticker là 50 callback mỗi frame, mỗi cái tự đọc thời gian, tự lên lịch.
- Tab nằm nền vài phút rồi quay lại: frame đầu tiên có delta hàng trăm nghìn mili giây, animation "nhảy" thẳng về cuối.
- Muốn dùng easing
cubic-bezier(.25,.1,.25,1)như CSS thì phải tự giải đường cong Bézier.
Engine Keyframes của Facebook (định dạng animation vector riêng, chạy trên canvas) giải cả ba trong hai module nhỏ: KeyframesLoop và CubicBezier.
Tip
Tóm tắt
- Mọi animation đăng ký vào một
Set, chỉ một vòng rAF lặp qua tất cả; set rỗng thì dừng rAF. - Delta mỗi frame bị kẹp tối đa 1000ms; khi
windownhậnfocus, mọi animation coi như frame trước cách đây đúng 16,67ms. CubicBeziergiảix(t) = xbằng Newton-Raphson 8 vòng, không hội tụ thì chuyển sang chia đôi (bisection).- Bản gốc có hai chỗ yếu: một callback ném lỗi làm cả vòng lặp dừng, và sai số 0.005 làm đường cong dốc lệch tới 0.15. Bản viết lại sửa cả hai.
▶ Mở demo toàn màn hình ↗ · Sửa code trên StackBlitz ↗ · Xem source trên GitHub ↗
#Facebook làm thế nào?
Hai module nằm trong thư mục AAA Keyframes/ của facebook-codebase (bản trùng ở Keyframes/).
#KeyframesLoop
Gần như nguyên văn, chỉ đổi tên biến:
var MAX_DELTA = 1e3, running = new Set();
function step(anim) {
var now = performanceNow();
anim.callback(Math.min(now - (anim.previousTime || now), MAX_DELTA)); // kẹp delta
anim.previousTime = now;
anim.framesRemaining -= 1;
anim.framesRemaining <= 0 && anim.cancel();
}
function tick() {
running.size > 0 && (running.forEach(step), requestAnimationFrame(tick));
}
window.addEventListener("focus", function () {
running.forEach(anim => anim.previousTime = performanceNow() - 16.67);
});
KeyframesLoop.prototype.start = function (frames = Infinity) {
this.framesRemaining = frames;
if (!this.$1) {
running.size === 0 && requestAnimationFrame(tick); // animation đầu tiên mới khởi động vòng
running.add(this);
this.previousTime = performanceNow();
this.$1 = TimeSlice.getGuardedContinuation("KeyframesLoop"); // để profiler nối ngữ cảnh
}
};
js
Ba ý đáng học:
- Một vòng duy nhất. Thêm animation đầu tiên thì gọi rAF; set rỗng thì
tickkhông hẹn frame mới nữa, vòng tự tắt. - Kẹp delta 1000ms. Trình duyệt hoãn rAF khi tab ẩn. Không kẹp thì frame đầu sau khi quay lại có delta bằng cả thời gian tab nằm nền.
- Reset khi
focus. Thêm một lớp nữa: khi cửa sổ được focus lại, mọi animation coi như frame trước vừa xảy ra cách đây đúng một frame 60Hz, nên frame kế tiếp tiến đúng 16,67ms chứ không phải 1000ms.
Ngoài ra, khi người dùng rời trang (Run.onLeave), mọi animation bị huỷ, trừ khi một cờ gatekeeper (gkx("708253")) tắt hành vi này.
#CubicBezier
var EPSILON = .005;
function CubicBezier(p1, p2) {
this.cx = 3 * p1[0]; this.bx = 3 * (p2[0] - p1[0]) - this.cx; this.ax = 1 - this.cx - this.bx;
this.cy = 3 * p1[1]; this.by = 3 * (p2[1] - p1[1]) - this.cy; this.ay = 1 - this.cy - this.by;
}
CubicBezier.prototype.solveCurveX = function (x) {
for (var t = x, i = 0; i < 8; i++) { // Newton-Raphson
var err = this.sampleCurveX(t) - x;
if (Math.abs(err) < EPSILON) return t;
var d = (3 * this.ax * t + 2 * this.bx) * t + this.cx;
if (Math.abs(d) < 1e-6) break;
t -= err / d;
}
// chia đôi trên [0, 1]
var lo = 0, hi = 1; t = x;
while (lo < hi) { /* ... */ }
};
js
Đây là cùng cấu trúc với UnitBezier mà các engine trình duyệt dùng cho cubic-bezier() của CSS: khai triển đa thức một lần trong constructor, Newton trước, chia đôi sau. KeyframesTween gọi nó cho mỗi đoạn tween kiểu Bezier. Có một chi tiết nhỏ: KeyframesTween tạo new CubicBezier(...) mỗi lần tính giá trị, nên phần khai triển hệ số chạy lại ở mỗi frame; tốn không nhiều nhưng cache lại thì miễn phí.
Facebook còn một solver thứ hai trong CometProgressRingUtils.getCubicBezierPercentageFunc (vòng tiến trình): chỉ Newton 4 vòng, không có chia đôi. Mình không tìm thấy chỗ gọi hàm này trong bản dump.
#Hai chỗ yếu
1. Một animation lỗi làm cả vòng lặp dừng. Trong tick, lệnh requestAnimationFrame(tick) nằm sau running.forEach(step) trong cùng một biểu thức. Nếu một callback ném lỗi, forEach dừng giữa chừng, rAF kế tiếp không bao giờ được hẹn, và mọi animation khác đứng hình theo. Demo phần 1: bấm "Cho spinner #5 ném lỗi" ở chế độ "Vòng chung kiểu bản gốc", cả 24 spinner dừng.
Còn một trường hợp hiếm hơn, mình chỉ suy ra từ code: nếu trong lúc forEach, animation cuối cùng tự cancel (set rỗng) rồi một animation khác start (thấy set rỗng nên gọi rAF), thì sau forEach, tick lại hẹn thêm một rAF nữa. Hai vòng chạy song song, mỗi frame mọi callback bị gọi hai lần.
2. Sai số 0.005 theo trục x. EPSILON là sai số cho phép của x (thời gian), nhưng thứ người dùng thấy là y. Ở đoạn đường cong dốc, sai một chút theo x thành sai nhiều theo y. Mình đo trên 1001 điểm so với nghiệm chính xác (bảng trong demo):
| Đường cong | Sai số y lớn nhất, ε = 0.005 | ε = 1e-4 |
|---|---|---|
ease |
0.0108 | 0.0002 |
ease-in-out |
0.0084 | 0.0001 |
(0.9, 0, 0.1, 1) |
0.0413 | 0.0006 |
(1, 0, 0, 1) |
0.1516 | 0.0007 |
Với ease, lệch 1% không thấy được. Với đường cong dựng đứng ở giữa, lệch 15% là một bước nhảy thấy rõ.
Solver 4 vòng của vòng tiến trình còn tệ hơn với đường cong (1, 0, 0, 1): Newton bị đạo hàm gần 0 kéo văng ra xa, kết quả y lên tới khoảng 30 nghìn (mình chạy lại bằng Node). Không có bước chia đôi để chặn lại.
flowchart TD
A["tick: rAF gọi"] --> B["duyệt Set animation"]
B --> C{"callback ném lỗi?"}
C -->|"bản gốc: có"| D["forEach dừng, không hẹn rAF mới"]
D --> E["mọi animation đứng hình"]
C -->|"bản viết lại: có"| F["bắt lỗi, bỏ animation đó, báo onError"]
F --> G["duyệt tiếp, hẹn rAF mới"]
C -->|"không"| G
#Bên trong trình duyệt: rAF khi tab ẩn
requestAnimationFrame chạy theo nhịp vẽ màn hình (thường 60Hz hoặc 120Hz). Khi tab bị ẩn, các trình duyệt chính tạm dừng rAF hẳn; khi tab hiện lại, callback đầu tiên nhận timestamp hiện tại. Không kẹp delta thì animation tính theo thời gian sẽ tua thẳng tới cuối. Kẹp ở 1000ms nghĩa là sau khi quay lại, animation tiến tối đa một giây rồi chạy tiếp bình thường.
Một vòng rAF chung còn có lợi về thứ tự: mọi animation đọc cùng một performance.now() trong frame đó và cập nhật DOM liền nhau, nên không có chuyện animation này đã sang frame mới còn animation kia chưa.
#Viết lại
Solver giữ thuật toán, thêm sai số tuỳ chỉnh (mặc định 1e-4) và giới hạn số vòng chia đôi:
src/lib/keyframes.ts
/** Tìm tham số t sao cho x(t) = x. */
solveCurveX(x: number, epsilon = 1e-4) {
// 1) Newton-Raphson: hội tụ rất nhanh khi đạo hàm không quá nhỏ
let t = x;
for (let i = 0; i < 8; i++) {
const error = this.sampleX(t) - x;
if (Math.abs(error) < epsilon) return t;
const slope = this.slopeX(t);
if (Math.abs(slope) < 1e-6) break; // gần chỗ đường cong "dựng đứng": Newton sẽ văng xa
t -= error / slope;
}
// 2) Phân đôi trên [0, 1]: chậm hơn nhưng chắc chắn hội tụ (x(t) đơn điệu khi x1, x2 ∈ [0, 1])
let lo = 0;
let hi = 1;
t = x;
if (t <= lo) return lo;
if (t >= hi) return hi;
for (let i = 0; i < 60 && lo < hi; i++) {
const value = this.sampleX(t);
if (Math.abs(value - x) < epsilon) return t;
if (x > value) lo = t;
else hi = t;
t = (hi - lo) / 2 + lo;
}
return t;
}
ts
Vòng lặp chung: chụp danh sách trước khi duyệt, bắt lỗi từng callback, và chỉ hẹn rAF khi chưa có cái nào đang chờ (rafId == null):
src/lib/keyframes.ts
function tick() {
rafId = null;
loopStats.rafCallbacks++;
const now = performance.now();
// chụp danh sách: animation cancel/start trong lúc lặp không làm hỏng vòng lặp này
for (const entry of [...running]) {
if (!running.has(entry)) continue;
const delta = Math.min(now - (entry.previousTime || now), MAX_DELTA);
entry.previousTime = now;
try {
entry.callback(delta);
} catch (error) {
// cô lập lỗi: báo ra ngoài nhưng các animation khác vẫn chạy tiếp
loopStats.errors++;
running.delete(entry);
loopConfig.onError(error);
continue;
}
entry.framesRemaining -= 1;
if (entry.framesRemaining <= 0) running.delete(entry);
}
schedule();
}
function schedule() {
if (running.size > 0 && rafId == null) rafId = requestAnimationFrame(tick);
}
ts
createLoop chỉ thêm/bớt phần tử trong running rồi gọi schedule(); xem toàn bộ trong spoiler bên dưới.
Khác bản gốc:
try/catchquanh từng callback; animation lỗi bị gỡ khỏi vòng, lỗi chuyển choloopConfig.onError.- Biến
rafIdchặn chuyện hai vòng chạy song song. - Không có phần nối ngữ cảnh profiler (
TimeSlice.getGuardedContinuation) và huỷ khi rời trang; dự án của bạn có router thì gọicancelở chỗ tương ứng.
Xem toàn bộ src/lib/keyframes.ts
src/lib/keyframes.ts
/**
* Engine animation bằng JS kiểu Keyframes của Facebook:
* - Một vòng requestAnimationFrame DÙNG CHUNG cho mọi animation (KeyframesLoop).
* - Kẹp delta tối đa 1000ms, và khi tab được focus lại thì coi như vừa qua đúng một frame,
* để animation không "nhảy cóc" sau khi tab nằm nền.
* - Easing cubic-bezier giải bằng Newton-Raphson + phân đôi (CubicBezier).
*
* Khác bản gốc:
* - Một callback ném lỗi không làm chết cả vòng lặp (bản gốc gọi rAF tiếp SAU forEach, lỗi
* giữa chừng làm mọi animation đứng hình).
* - Không thể có hai vòng rAF song song khi animation tự cancel rồi start trong callback.
* - Sai số mặc định 1e-4 thay vì 0.005 (với đường cong dốc, 0.005 theo trục x có thể lệch
* tới 0.15 theo trục y).
*/
// ---- Cubic bezier ----
export class CubicBezier {
private ax: number;
private bx: number;
private cx: number;
private ay: number;
private by: number;
private cy: number;
constructor(p1: [number, number], p2: [number, number]) {
// Khai triển đa thức Bézier B(t) = ((a t + b) t + c) t cho từng trục, tính MỘT lần.
this.cx = 3 * p1[0];
this.bx = 3 * (p2[0] - p1[0]) - this.cx;
this.ax = 1 - this.cx - this.bx;
this.cy = 3 * p1[1];
this.by = 3 * (p2[1] - p1[1]) - this.cy;
this.ay = 1 - this.cy - this.by;
}
private sampleX(t: number) {
return ((this.ax * t + this.bx) * t + this.cx) * t;
}
private sampleY(t: number) {
return ((this.ay * t + this.by) * t + this.cy) * t;
}
private slopeX(t: number) {
return (3 * this.ax * t + 2 * this.bx) * t + this.cx;
}
/** Tìm tham số t sao cho x(t) = x. */
solveCurveX(x: number, epsilon = 1e-4) {
// 1) Newton-Raphson: hội tụ rất nhanh khi đạo hàm không quá nhỏ
let t = x;
for (let i = 0; i < 8; i++) {
const error = this.sampleX(t) - x;
if (Math.abs(error) < epsilon) return t;
const slope = this.slopeX(t);
if (Math.abs(slope) < 1e-6) break; // gần chỗ đường cong "dựng đứng": Newton sẽ văng xa
t -= error / slope;
}
// 2) Phân đôi trên [0, 1]: chậm hơn nhưng chắc chắn hội tụ (x(t) đơn điệu khi x1, x2 ∈ [0, 1])
let lo = 0;
let hi = 1;
t = x;
if (t <= lo) return lo;
if (t >= hi) return hi;
for (let i = 0; i < 60 && lo < hi; i++) {
const value = this.sampleX(t);
if (Math.abs(value - x) < epsilon) return t;
if (x > value) lo = t;
else hi = t;
t = (hi - lo) / 2 + lo;
}
return t;
}
/** Tiến độ thời gian x ∈ [0, 1] -> tiến độ animation y. */
solve(x: number, epsilon?: number) {
return this.sampleY(this.solveCurveX(x, epsilon));
}
}
export const cubicBezier = (x1: number, y1: number, x2: number, y2: number) => {
const curve = new CubicBezier([x1, y1], [x2, y2]);
return (x: number) => curve.solve(x);
};
// ---- Vòng rAF dùng chung ----
const MAX_DELTA = 1000;
const ONE_FRAME = 1000 / 60;
export interface LoopHandle {
/** chạy tối đa `frames` frame rồi tự dừng (mặc định chạy mãi) */
start(frames?: number): void;
cancel(): void;
isRunning(): boolean;
}
interface Entry {
callback: (deltaMs: number) => void;
previousTime: number;
framesRemaining: number;
}
const running = new Set<Entry>();
let rafId: number | null = null;
let focusListenerInstalled = false;
/** số lần callback rAF thật sự được gọi, để demo đếm */
export const loopStats = { rafCallbacks: 0, errors: 0 };
/** Nơi nhận lỗi của animation; production có thể nối vào error logger. */
export const loopConfig = {
onError: (error: unknown) => console.error(error),
};
function tick() {
rafId = null;
loopStats.rafCallbacks++;
const now = performance.now();
// chụp danh sách: animation cancel/start trong lúc lặp không làm hỏng vòng lặp này
for (const entry of [...running]) {
if (!running.has(entry)) continue;
const delta = Math.min(now - (entry.previousTime || now), MAX_DELTA);
entry.previousTime = now;
try {
entry.callback(delta);
} catch (error) {
// cô lập lỗi: báo ra ngoài nhưng các animation khác vẫn chạy tiếp
loopStats.errors++;
running.delete(entry);
loopConfig.onError(error);
continue;
}
entry.framesRemaining -= 1;
if (entry.framesRemaining <= 0) running.delete(entry);
}
schedule();
}
function schedule() {
if (running.size > 0 && rafId == null) rafId = requestAnimationFrame(tick);
}
function installFocusListener() {
if (focusListenerInstalled || typeof window === 'undefined') return;
focusListenerInstalled = true;
// Tab vừa quay lại: coi như frame trước cách đây đúng 1 frame, tránh delta khổng lồ
window.addEventListener('focus', () => {
const now = performance.now();
running.forEach((entry) => (entry.previousTime = now - ONE_FRAME));
});
}
export function createLoop(callback: (deltaMs: number) => void): LoopHandle {
const entry: Entry = { callback, previousTime: 0, framesRemaining: Infinity };
installFocusListener();
return {
start(frames = Infinity) {
entry.framesRemaining = frames;
if (running.has(entry)) return;
entry.previousTime = performance.now();
running.add(entry);
schedule();
},
cancel() {
running.delete(entry);
},
isRunning: () => running.has(entry),
};
}
/** Chỉ để demo/test: giả lập tab vừa nằm nền `ms` mili giây (frame kế tiếp có delta lớn). */
export function simulateBackground(ms: number) {
running.forEach((entry) => (entry.previousTime -= ms));
}
/** Tiện ích: tween một giá trị 0 -> 1 trong `duration` ms với easing cho trước. */
export function tween(
duration: number,
easing: (x: number) => number,
onUpdate: (value: number) => void,
onDone?: () => void,
): LoopHandle {
let elapsed = 0;
const handle = createLoop((delta) => {
elapsed = Math.min(elapsed + delta, duration);
onUpdate(easing(elapsed / duration));
if (elapsed >= duration) {
handle.cancel();
onDone?.();
}
});
handle.start();
return handle;
}
ts
#Tại sao không làm cách khác?
Dùng Web Animations API (element.animate)? Nếu animation chỉ là thuộc tính CSS của một element thì nên dùng: trình duyệt tự lo easing, tự dừng khi tab ẩn, có thể chạy trên compositor. Vòng rAF tự viết dành cho thứ WAAPI không làm được: vẽ canvas, cập nhật state JS, animation điều khiển bởi dữ liệu.
Mỗi component tự gọi rAF? Đơn giản, nhưng N animation là N callback, mỗi cái tự đọc thời gian, và không có chỗ chung để kẹp delta hay bắt lỗi. Demo đếm: 24 spinner tự gọi rAF là 1440 callback mỗi giây; vòng chung là 60.
Dùng thư viện easing có sẵn? Hoàn toàn được (ví dụ gói bezier-easing trên npm cũng là Newton + chia đôi). Đoạn code chỉ khoảng 50 dòng nên tự viết cũng không tốn mấy.
#Edge case và cạm bẫy
- Màn hình 120Hz. Hằng số 16,67ms khi
focusgiả định 60Hz. Trên màn 120Hz, frame đầu sau focus tiến gấp đôi một frame thật; không đáng kể, nhưng nếu cần chính xác thì đo khoảng cách frame trung bình. focuskhông phảivisibilitychange. Chuyển tab rồi quay lại thì có cả hai sự kiện; nhưng cửa sổ khác che lên rồi bỏ ra (tab vẫn hiển thị) thì cófocusmà không cóvisibilitychange. Kẹp delta 1000ms là lưới an toàn cho mọi trường hợp còn lại.- Animation hữu hạn frame.
start(frames)chạy đúng N frame bất kể thời gian thực. Dùng cho hiệu ứng tính theo frame; animation tính theo thời gian thì dùngtween. - Đường cong có y ngoài [0, 1] (kiểu "back", vượt quá đích) vẫn giải đúng, vì chỉ x phải nằm trong [0, 1].
#Dùng trong dự án
import { cubicBezier, tween } from './keyframes';
const ease = cubicBezier(0.25, 0.1, 0.25, 1);
const handle = tween(600, ease, (v) => {
ctx.clearRect(0, 0, w, h);
drawSticker(ctx, v); // vẽ canvas theo tiến độ v
});
// huỷ khi component unmount
return () => handle.cancel();
ts
#Kiểm thử
Ví dụ test (bản phác, không có trong repo)
import { describe, expect, it, vi } from 'vitest';
import { CubicBezier, createLoop, loopConfig } from './keyframes';
describe('CubicBezier', () => {
it('khớp các điểm đầu cuối và đối xứng của ease-in-out', () => {
const c = new CubicBezier([0.42, 0], [0.58, 1]);
expect(c.solve(0)).toBeCloseTo(0, 6);
expect(c.solve(1)).toBeCloseTo(1, 6);
expect(c.solve(0.5)).toBeCloseTo(0.5, 3);
});
it('sai số nhỏ với đường cong dựng đứng', () => {
const c = new CubicBezier([1, 0], [0, 1]);
for (let x = 0; x <= 1; x += 0.01) {
expect(Math.abs(c.solve(x) - c.solve(x, 1e-12))).toBeLessThan(0.002);
}
});
});
describe('createLoop', () => {
it('một callback lỗi không làm dừng các animation khác', async () => {
let raf: FrameRequestCallback | null = null;
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => ((raf = cb), 1));
const onError = vi.fn();
loopConfig.onError = onError;
const good = vi.fn();
createLoop(() => { throw new Error('hỏng'); }).start();
createLoop(good).start();
raf!(0);
raf!(16);
expect(onError).toHaveBeenCalledTimes(1);
expect(good).toHaveBeenCalledTimes(2);
});
});
ts
#So với chuẩn web và thư viện
- Web Animations API và CSS
transition/animationnên là lựa chọn đầu tiên cho animation thuộc tính CSS; chúng hỗ trợ sẵn mọi hàmcubic-bezier()vàlinear(). - Các thư viện animation JS phổ biến (GSAP, Motion, anime.js…) đều dùng một vòng
requestAnimationFrametrung tâm (GSAP gọi là "ticker") thay vì mỗi animation một vòng, cùng ý tưởng vớiKeyframesLoop.
Bài này thuộc series Học từ mã nguồn Facebook (106 bài, đọc theo thứ tự).
Xem nhanh danh sách bài trong series
Phần 1
- Mở đầu: cách đọc code minify của Facebook
- #1 Dùng chung IntersectionObserver + infinite scroll
- #2 useViewportDuration: đo thời gian thực sự được xem
- #3 Preload code + data khi hover
- #4 Toast manager với external store
- #5 Network status + heartbeat backoff
- #6 ErrorGuard: bắt lỗi theo ngữ cảnh
- #7 Banzai: gom log theo lô + sendBeacon
- #8 Focus trap + Esc theo layer
Phần 2
- #9 Mẹo calc(… * 9999): container query, line-clamp, mũi tên carousel bằng CSS
- #10 Thanh cuộn tuỳ biến bằng CSS 3D
- #11 Modal cuộn bằng window, giữ vị trí trang nền
- #12 Popover, tooltip group và hovercard
- #13 Primitive Pressable + focus-visible
- #14 Tab "Xem thêm" + sticky header bằng IntersectionObserver
- #15 Avatar khoét mask, gradient Messenger, skeleton steps()
- #16 Khung chat không giật
- #17 Thuật toán chọn bố cục collage ảnh
- #18 Keep-alive tab trong SPA
- #19 Tự đo Visual Completion & interaction tracing
- #20 Router SPA bền bỉ & tải chunk có retry
Phần 3
- #21 Smart Compose: gợi ý chữ mờ khi gõ
- #22 MQTT ngay trong trình duyệt, tự chuyển sang long-poll
- #23 Đo thời gian dùng bằng bitmap 64 giây
- #24 Bảng reaction nhấn giữ rồi trượt
- #25 Tìm kiếm gấp dấu, phiên âm và highlight đầu từ
- #26 Ring buffer biết trace nào đã mất log
- #27 Engine animation JS: một vòng rAF và solver cubic-bezier (bài này)
- #28 Bootloader: bitmap tài nguyên đã tải, chỉ reload khi cần
- #29 Nhận biết click do screen reader tạo ra
- #30 Tải trước feed theo chất lượng mạng, đoán hạng thiết bị
- #31 Hàng đợi gửi tin chat giữ đúng thứ tự
- #32 Gộp lời gọi API trong cùng một tick
- #33 Resize ảnh trong pool Web Worker
- #34 Crop ảnh theo nhiều khuôn mặt
- #35 Kéo-thả sắp xếp và vùng thả file không nhấp nháy
- #36 Tiện ích UI: fade-out, sticky event, hàm huỷ được
- #37 Hạ tầng nhỏ: cookie, hash lỗi, prefetch khi chuột dừng
Phần 4–6
- #38 Đồng bộ tin nhắn: phát hiện lỗ seqID và fetch bù theo tầng
- #39 OTID: ID tin nhắn kiểu Snowflake sinh ở client
- #40 QPL: lấy mẫu từ lúc bắt đầu, đo RTT gửi tin đầu-cuối
- #41 Bộ điều phối request bảy chế độ gộp và chống trùng
- #42 Thread list theo mốc thời gian, cookie presence có version
- #43 Composer dạng plugin: reducer, dirty checker, chuỗi kiểm tra
- #44 Thanh toán bất đồng bộ: subscribe trước, poll dự phòng
- #45 Validate số thẻ: sửa lỗi gõ, dải BIN, Luhn
- #46 Lịch chọn khoảng ngày: bo góc dải chọn, phím đủ bộ, RTL
- #47 Giờ mở cửa theo tuần: qua đêm, gộp khoảng, phần bù
- #48 Tiến độ đăng realtime: giữ event chờ snapshot, watchdog
- #49 Tiền tệ theo đơn vị nhỏ nhất và ô nhập tiền
- #50 Sửa hàng loạt Empty/Uniform/Mixed, seen/read bằng 2 bit
- #51 Phát hiện script lạ hook vào hàm native
- #52 localStorage tự dọn khi đầy quota
- #53 Cache tuyến do server phân giải, chờ sẵn promise
- #54 Lưới phím mũi tên 2D và focus không làm cuộn trang
- #55 Tách emoji bằng máy trạng thái
- #56 Đọc trước ghi sau trong một frame, con tự biết vị trí
- #57 Hàng đợi lệnh cho video và stream sắp lại chunk
Phần 7–10
- #58 Ước lượng băng thông EWMA: loại outlier, nhớ qua phiên
- #59 ABR theo độ tự tin phát và cắt bậc bitrate theo MOS
- #60 Prefetch segment dự đoán từ manifest, parse hộp sidx
- #61 Đọc metadata MP4/MOV ngay trên client
- #62 Upload tiếp tục được: offset, hash dedupe, phát hiện treo
- #63 Fenwick tree: chọn ngẫu nhiên có trọng số và offset dòng
- #64 Vạch trục số và trục thời gian "đẹp"
- #65 Damerau-Levenshtein theo dải chéo và tự sửa tên gõ sai
- #66 PRNG có seed, shuffle tái lập và lấy mẫu trace
- #67 Cặp min/max-heap tái dùng vị trí trong danh sách ảo
- #68 Memo nhiều đối số bằng Map/WeakMap và interning object
- #69 Thống kê cửa sổ trượt và đại số khoảng thời gian
- #70 Int64 từ hai số 32-bit, decode UTF, offset theo code point
- #71 Engine múi giờ IANA trên client, phát hiện lệch giờ
- #72 Tự cài semver range
- #73 Auto-link URL, cắt chữ an toàn, giới hạn LaTeX
- #74 Parser query kiểu PHP chống prototype pollution
- #75 Popover tự lật theo phần tràn nhỏ nhất
- #76 Tiền tố for(;😉 chống JSON hijacking, đọc stream
- #77 Chống lộ Referer, linkshim, xoá token trên URL
- #78 setImmediate bằng postMessage, gom delta chuột theo frame
- #79 Phát hiện crash qua heartbeat, dò proxy cache hỏng
- #80 DPR vào cookie cho ảnh @2x, che khoá trong log
- #81 Flux waitFor tự viết và proxy có đệm
- #82 Crop đúng tỉ lệ, xoay sticker bằng ma trận
Phần 11–15
- #83 Chia subdomain giữa các tab bằng lease, long-poll theo bảng trạng thái
- #84 MQTT QoS1: giữ publish chờ ack, gửi lại khi kết nối lại
- #85 Cache Promise gộp request, cache lỗi ngắn, đua dữ liệu nhúng sẵn
- #86 Lịch retry: backoff có trần, Fibonacci, báo lỗi CDN mỗi N ngày
- #87 Giữ nguyên tham chiếu nhánh dữ liệu không đổi
- #88 Store optimistic: copy-on-write, rollback rồi áp lại
- #89 Chỉ mục subscription theo record, hàng đợi task theo chiều sâu
- #90 Phân trang connection: cursor khớp đầu, edge không trùng
- #91 Profiler kiểu middleware và memo selector có lấy mẫu kiểm tra
- #92 Đọc EBML/WebM Cues để tìm byte range của segment
- #93 Stream HTTP Range tạm dừng được, tải tiếp đúng byte
- #94 Bảng khoảng thời gian gắn nhãn và nhảy qua lỗ buffer
- #95 Gom chunk cho SourceBuffer, đồng bộ video bằng playbackRate
- #96 Gom tin nhắn thành cụm, vạch ngày và avatar đã xem
- #97 Bao nhiêu tab chat vừa màn hình, chuỗi luật mở tab
- #98 Phím tắt Markdown trong editor, Backspace để hoàn tác
- #99 Offset theo code point hay UTF-16: bốn bug cùng một gốc
- #100 Typeahead không nhảy: cache tiền tố, chặn reflow, khoá hover
- #101 Hit-test trên canvas bằng phát lại path, nét đứt và cung Bézier
- #102 Vòng tròn theo mét trên bản đồ Mercator
- #103 Dò tính năng trình duyệt: ảnh EXIF 2×1, getter bẫy, scrollbar
- #104 Bug định dạng số và kiểm tra dữ liệu theo locale
- #105 Dispose cho đúng: idle callback, splice(-1), xoá nhầm key
Code + demo: examples/facebook-patterns · Mở trên StackBlitz

