Học từ Facebook #66: PRNG có seed, xáo trộn tái lập và lấy mẫu trace 1/32
Facebook mang theo Alea, một bộ sinh số ngẫu nhiên nhận seed, để xáo trộn danh sách giống nhau mỗi lần tải trang và sinh ID trace 63 bit mà ký tự cuối quyết định có lấy mẫu hay không. Bài viết giải mã, chỉ ra seed mặc định chỉ tới giây và việc xáo tại chỗ.
Math.random() không nhận seed. Muốn cùng đầu vào thì ra cùng dãy ngẫu nhiên là bó tay:
- Xáo thứ tự câu hỏi cho mỗi thí sinh, tải lại trang vẫn giữ thứ tự cũ, không phải lưu thứ tự lên server.
- Chia người dùng vào nhóm A/B ổn định chỉ từ
userId. - Test hay demo có yếu tố ngẫu nhiên nhưng cần chạy lại y hệt khi gỡ lỗi.
Bundle Facebook mang theo Alea, một bộ sinh số giả ngẫu nhiên (PRNG) nhỏ của Johannes Baagøe (giấy phép MIT), cùng hai chỗ dùng nó: randomShuffleSeeded và ArtilleryTraceIDUtils (ID cho hệ thống trace hiệu năng Artillery).
Tip
Tóm tắt
- Alea giữ 3 trạng thái số thực + 1 số nhớ, mỗi lần gọi là một phép nhân-cộng với nhớ (multiply-with-carry). Hàm băm Mash biến seed (chuỗi bất kỳ) thành trạng thái ban đầu.
- Xáo trộn có seed = Fisher-Yates với PRNG có seed. Bản Facebook xáo tại chỗ: gọi hai lần trên cùng mảng ra hai thứ tự khác nhau.
- Seed mặc định của Alea là
new Date(), bị Mash đổi thành chuỗi chỉ tới giây: hai bộ sinh tạo trong cùng một giây cho cùng dãy. - ID trace: 11 ký tự base64 = 63 bit (ký tự đầu chỉ 3 bit để vừa int64 có dấu). Lấy mẫu 1/32 bằng ký tự cuối mod 32, không cần sinh thêm số ngẫu nhiên, và server, client cùng quyết định giống nhau.
▶ 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?
#Mash và Alea
Module Alea (có trong bundle 1.js, 22.js) gần như nguyên văn bản gốc của Baagøe:
function Mash() {
let n = 4022871197; // 0xefc8249d
return function (data) {
data = data.toString();
for (let i = 0; i < data.length; i++) {
n += data.charCodeAt(i);
let h = 0.02519603282416938 * n;
n = h >>> 0; // phần nguyên 32 bit
h -= n; // phần thập phân
h *= n;
n = h >>> 0;
h -= n;
n += h * 4294967296; // 2^32
}
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 → [0, 1)
};
}
function Alea(...args) {
const seeds = args.length > 0 ? args : [new Date()];
const mash = Mash();
let s0 = mash(' '), s1 = mash(' '), s2 = mash(' '), c = 1;
for (const seed of seeds) {
s0 -= mash(seed); if (s0 < 0) s0 += 1;
s1 -= mash(seed); if (s1 < 0) s1 += 1;
s2 -= mash(seed); if (s2 < 0) s2 += 1;
}
return function random() {
const t = 2091639 * s0 + c * 2.3283064365386963e-10;
s0 = s1;
s1 = s2;
return (s2 = t - (c = t | 0));
};
}
js
Mỗi lần gọi random(): nhân trạng thái cũ nhất với hằng số 2091639, cộng số nhớ c (đã chia cho 2³²), phần nguyên của kết quả thành số nhớ mới, phần thập phân là số trả về và trở thành trạng thái mới nhất. Ba trạng thái xoay vòng (s0 ← s1 ← s2 ← mới). Đây là phép multiply-with-carry "lag-3", viết bằng số thực để chạy nhanh trên JavaScript mà không cần số nguyên 64 bit. Chu kỳ khoảng 2¹¹⁶ theo tác giả, quá đủ cho UI.
Mash cũng đáng chú ý: nó băm mọi seed (số, chuỗi, Date) qua toString() thành số thực trong [0, 1), và giữ trạng thái n giữa các lần gọi. Vì thế nhiều seed (Alea('quiz-10', 'user-42')) được trộn theo thứ tự: Alea('a', 'b') khác Alea('b', 'a').
Hệ quả của toString(): seed mặc định new Date() thành "Wed Oct 28 2026 09:47:13 GMT+0000 (…)", không có mili-giây. Hai bộ sinh không truyền seed tạo trong cùng một giây (hai tab mở cùng lúc, hai component khởi tạo cùng lượt render) cho cùng một dãy số. Demo có thử điều này với hai thời điểm cách nhau 900ms.
#randomShuffleSeeded
function randomShuffleSeeded(array, ...seeds) {
const random = Alea(...seeds);
const randomInt = (max) => Math.floor(random() * max);
for (let h = array.length - 1; h > 0; h--) {
const i = randomInt(h + 1); // 0 ≤ i ≤ h
if (i != h) [array[i], array[h]] = [array[h], array[i]];
}
return array; // chính mảng đầu vào
}
js
Đây là Fisher-Yates chuẩn: đi từ cuối về đầu, đổi phần tử h với một phần tử ngẫu nhiên trong [0, h]. Mọi hoán vị có xác suất bằng nhau (nếu PRNG đều). Cạm bẫy: hàm sửa mảng đầu vào. Gọi hai lần với cùng seed trên cùng một mảng không ra cùng thứ tự, vì lần hai xáo một mảng đã bị xáo. Ai lấy mảng từ state React hay từ cache dùng chung mà quên .slice() sẽ gặp lỗi khó hiểu: "cùng seed mà sao thứ tự đổi?".
#ArtilleryTraceIDUtils: ID 63 bit và lấy mẫu bằng ký tự cuối
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const random = Alea(ServerNonce.ServerNonce); // seed là nonce server gửi kèm trang
function once() {
let bits = 0, pool = 0, id = '';
for (let i = 10; i >= 0; i--) {
if (bits < 6) { pool = Math.floor(2147483648 * random()); bits = 32; } // 2^31
if (i === 0) pool &= 7; // ký tự đầu chỉ 3 bit
id = BASE64.charAt(pool & 63) + id;
pool >>= 6;
bits -= 6;
}
return id;
}
newTraceId() {
for (let n = 3; n > 0; n--) {
const id = once();
if (id != 'AAAAAAAAAAA') return id; // ID toàn 0 dành cho "không có trace"
}
FBLogger('FIXME').mustfix('failed to generate valid Fbtrace id');
return 'AAAAAAAAAAA';
}
traceIdToBucket(id) {
return BASE64.indexOf(id.charAt(id.length - 1)) % 32;
}
js
- 63 bit: 10 ký tự × 6 bit + 3 bit = 63. ID đổi ngược ra số vừa kiểu
BIGINTcó dấu của MySQL/Java mà không bao giờ âm. - Seed là
ServerNonce(một chuỗi ngẫu nhiên server nhúng vào trang), không phải thời gian, nên hai tab có ID khác nhau. - Lấy mẫu theo ký tự cuối: ký tự cuối mang 6 bit ngẫu nhiên,
% 32cho nhóm 0–31 đều nhau. Hệ thống chỉ cần quy định "giữ trace có nhóm < N" để lấy mẫu N/32. Vì quyết định nằm trong ID, mọi tầng (client, proxy, backend) nhìn cùng một ID đều đồng ý trace này được giữ hay bỏ, không cần truyền thêm cờ. - Một chi tiết nhỏ: bộ đếm
bits = 32trong khiMath.floor(2^31 * random())chỉ có 31 bit. Không ảnh hưởng kết quả: sau 5 ký tự (30 bit) bộ đếm còn 2, dưới 6 nên vẫn lấy số mới.
#Viết lại
Bản viết lại giữ nguyên Mash/Alea (thuật toán công khai, cần đúng hằng số để tái lập dãy), đổi phần xung quanh: xáo trên bản sao, seed mặc định bằng crypto.getRandomValues, thêm bucketOf cho A/B.
src/lib/seededRandom.ts
/** Hàm băm Mash: biến chuỗi thành số trong [0, 1). Giữ trạng thái giữa các lần gọi. */
function createMash() {
let n = 0xefc8249d; // 4022871197
return (data: string) => {
for (let i = 0; i < data.length; i++) {
n += data.charCodeAt(i);
let h = 0.02519603282416938 * n;
n = h >>> 0;
h -= n;
h *= n;
n = h >>> 0;
h -= n;
n += h * 0x100000000; // 2^32
}
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
};
}
export type Random = () => number;
/** Tạo PRNG Alea từ một hoặc nhiều seed. Cùng seed → cùng dãy số. */
export function alea(...seeds: Array<string | number>): Random {
const mash = createMash();
let s0 = mash(' ');
let s1 = mash(' ');
let s2 = mash(' ');
let carry = 1;
for (const seed of seeds) {
const text = String(seed);
s0 -= mash(text);
if (s0 < 0) s0 += 1;
s1 -= mash(text);
if (s1 < 0) s1 += 1;
s2 -= mash(text);
if (s2 < 0) s2 += 1;
}
return () => {
const t = 2091639 * s0 + carry * 2.3283064365386963e-10;
s0 = s1;
s1 = s2;
return (s2 = t - (carry = t | 0));
};
}
ts
src/lib/seededRandom.ts
/** Seed khó trùng khi không truyền seed: bản gốc dùng new Date() (Mash đổi ra chuỗi, chỉ tới giây), hai tab mở trong cùng một giây sẽ trùng dãy. */
export function randomSeed() {
const buf = new Uint32Array(2);
crypto.getRandomValues(buf);
return `${buf[0].toString(36)}${buf[1].toString(36)}`;
}
export function randomInt(random: Random, maxExclusive: number) {
return Math.floor(random() * maxExclusive);
}
/** Fisher-Yates trên bản sao (bản gốc xáo trộn tại chỗ, dễ làm hỏng mảng của người gọi). */
export function shuffleSeeded<T>(items: readonly T[], ...seeds: Array<string | number>) {
const random = alea(...seeds);
const out = items.slice();
for (let i = out.length - 1; i > 0; i--) {
const j = randomInt(random, i + 1);
[out[i], out[j]] = [out[j], out[i]];
}
return out;
}
/** Chia người dùng vào nhóm A/B ổn định: cùng (experiment, userId) luôn ra cùng nhóm. */
export function bucketOf(experiment: string, userId: string, buckets: number) {
return randomInt(alea(experiment, userId), buckets);
}
ts
ID trace và nhóm lấy mẫu:
src/lib/seededRandom.ts
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const DUMMY_TRACE_ID = 'AAAAAAAAAAA';
/**
* ID trace 11 ký tự base64 = 63 bit: ký tự đầu chỉ 3 bit (0..7) để cả số vừa int64 có dấu.
* Mỗi lần lấy 31 bit từ PRNG, cắt 6 bit một.
*/
export function createTraceIdGenerator(random: Random) {
function once() {
let bits = 0;
let pool = 0;
let id = '';
for (let i = 10; i >= 0; i--) {
if (bits < 6) {
pool = Math.floor(0x80000000 * random());
bits = 31;
}
if (i === 0) pool &= 7;
id = BASE64.charAt(pool & 63) + id;
pool >>= 6;
bits -= 6;
}
return id;
}
return () => {
// giống bản gốc: thử lại vài lần nếu ra đúng ID "rỗng" dùng làm giá trị giả
for (let attempt = 0; attempt < 3; attempt++) {
const id = once();
if (id !== DUMMY_TRACE_ID) return id;
}
return DUMMY_TRACE_ID;
};
}
/** Nhóm 0..31 suy từ ký tự cuối của ID: lấy mẫu 1/32 mà không cần thêm số ngẫu nhiên. */
export function traceIdToBucket(traceId: string) {
return BASE64.indexOf(traceId.charAt(traceId.length - 1)) % 32;
}
ts
Xem toàn bộ src/lib/seededRandom.ts
src/lib/seededRandom.ts
/**
* Số ngẫu nhiên tái lập được theo seed (ý tưởng từ Alea, randomShuffleSeeded và
* ArtilleryTraceIDUtils trong bundle Facebook).
*
* Alea là thuật toán công khai của Johannes Baagøe (giấy phép MIT); Facebook dùng nguyên
* hằng số của tác giả. KHÔNG dùng cho mật khẩu, token, ID bí mật: dùng crypto.getRandomValues.
*/
/** Hàm băm Mash: biến chuỗi thành số trong [0, 1). Giữ trạng thái giữa các lần gọi. */
function createMash() {
let n = 0xefc8249d; // 4022871197
return (data: string) => {
for (let i = 0; i < data.length; i++) {
n += data.charCodeAt(i);
let h = 0.02519603282416938 * n;
n = h >>> 0;
h -= n;
h *= n;
n = h >>> 0;
h -= n;
n += h * 0x100000000; // 2^32
}
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
};
}
export type Random = () => number;
/** Tạo PRNG Alea từ một hoặc nhiều seed. Cùng seed → cùng dãy số. */
export function alea(...seeds: Array<string | number>): Random {
const mash = createMash();
let s0 = mash(' ');
let s1 = mash(' ');
let s2 = mash(' ');
let carry = 1;
for (const seed of seeds) {
const text = String(seed);
s0 -= mash(text);
if (s0 < 0) s0 += 1;
s1 -= mash(text);
if (s1 < 0) s1 += 1;
s2 -= mash(text);
if (s2 < 0) s2 += 1;
}
return () => {
const t = 2091639 * s0 + carry * 2.3283064365386963e-10;
s0 = s1;
s1 = s2;
return (s2 = t - (carry = t | 0));
};
}
/** Seed khó trùng khi không truyền seed: bản gốc dùng new Date() (Mash đổi ra chuỗi, chỉ tới giây), hai tab mở trong cùng một giây sẽ trùng dãy. */
export function randomSeed() {
const buf = new Uint32Array(2);
crypto.getRandomValues(buf);
return `${buf[0].toString(36)}${buf[1].toString(36)}`;
}
export function randomInt(random: Random, maxExclusive: number) {
return Math.floor(random() * maxExclusive);
}
/** Fisher-Yates trên bản sao (bản gốc xáo trộn tại chỗ, dễ làm hỏng mảng của người gọi). */
export function shuffleSeeded<T>(items: readonly T[], ...seeds: Array<string | number>) {
const random = alea(...seeds);
const out = items.slice();
for (let i = out.length - 1; i > 0; i--) {
const j = randomInt(random, i + 1);
[out[i], out[j]] = [out[j], out[i]];
}
return out;
}
/** Chia người dùng vào nhóm A/B ổn định: cùng (experiment, userId) luôn ra cùng nhóm. */
export function bucketOf(experiment: string, userId: string, buckets: number) {
return randomInt(alea(experiment, userId), buckets);
}
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const DUMMY_TRACE_ID = 'AAAAAAAAAAA';
/**
* ID trace 11 ký tự base64 = 63 bit: ký tự đầu chỉ 3 bit (0..7) để cả số vừa int64 có dấu.
* Mỗi lần lấy 31 bit từ PRNG, cắt 6 bit một.
*/
export function createTraceIdGenerator(random: Random) {
function once() {
let bits = 0;
let pool = 0;
let id = '';
for (let i = 10; i >= 0; i--) {
if (bits < 6) {
pool = Math.floor(0x80000000 * random());
bits = 31;
}
if (i === 0) pool &= 7;
id = BASE64.charAt(pool & 63) + id;
pool >>= 6;
bits -= 6;
}
return id;
}
return () => {
// giống bản gốc: thử lại vài lần nếu ra đúng ID "rỗng" dùng làm giá trị giả
for (let attempt = 0; attempt < 3; attempt++) {
const id = once();
if (id !== DUMMY_TRACE_ID) return id;
}
return DUMMY_TRACE_ID;
};
}
/** Nhóm 0..31 suy từ ký tự cuối của ID: lấy mẫu 1/32 mà không cần thêm số ngẫu nhiên. */
export function traceIdToBucket(traceId: string) {
return BASE64.indexOf(traceId.charAt(traceId.length - 1)) % 32;
}
ts
#Đi qua code
createMashviết lại bằng hằng số hex (0xefc8249d= 4022871197) cho dễ tra. Mọi phép>>> 0lấy 32 bit thấp của phần nguyên; phép nhân với0.0251…rồi tách nguyên/thập phân hai lần là cách Baagøe trộn bit chỉ bằng số thực.alea(...seeds)bắt buộc truyền seed (không có mặc định ngầm). Muốn ngẫu nhiên thật thì gọialea(randomSeed()), để việc "không tái lập" là quyết định có chủ ý.randomSeedlấy 64 bit từcrypto.getRandomValues, đổi ra chuỗi base36. Có thể in seed ra log: khi người dùng báo lỗi, chạy lại đúng seed đó.shuffleSeedednhậnreadonly T[], copy bằngslice()rồi Fisher-Yates trên bản sao. TypeScript chặn luôn việc truyền mảng rồi trông đợi nó bị sửa.bucketOf(experiment, userId, n)= số đầu tiên củaalea(experiment, userId)nhânn. Thêm tên thí nghiệm vào seed để mỗi thí nghiệm chia người dùng độc lập nhau: người ở nhóm 0 của thí nghiệm này không mặc nhiên ở nhóm 0 của thí nghiệm khác. Demo chia 20.000 người vào 4 nhóm ra khoảng 5.000 mỗi nhóm.createTraceIdGenerator(random)nhận PRNG từ ngoài (dễ test với seed cố định), đặtbits = 31cho đúng số bit thật.
Khác bản gốc:
- Xáo trên bản sao, không sửa mảng đầu vào.
- Không còn seed mặc định
new Date()(độ phân giải giây). - Bỏ phần ghi log khi sinh ID thất bại 3 lần (xác suất
2^-63mỗi lần, thực tế không xảy ra).
#Tại sao không làm cách khác?
Băm userId (FNV, murmur) rồi mod số nhóm? Với A/B chỉ cần một số ngẫu nhiên, hàm băm là đủ và nhanh hơn. PRNG có seed hơn ở chỗ sinh được cả dãy (xáo trộn, chọn nhiều phần tử, mô phỏng).
crypto.getRandomValues cho mọi thứ? Không có seed nên không tái lập được. Dùng nó cho thứ cần không đoán được (token, ID phiên, mật khẩu, nonce bảo mật); dùng PRNG có seed cho thứ cần tái lập được. Alea không an toàn mật mã: biết vài số đầu ra có thể suy ngược trạng thái.
Mulberry32, xoshiro128**, sfc32? Đều là PRNG nhỏ tốt, có seed, chất lượng thống kê tốt hơn hoặc ngang Alea, và nhận seed 32/128 bit thay vì chuỗi. Alea tiện ở chỗ seed là chuỗi bất kỳ nhờ Mash. Chọn cái nào cũng được, miễn cố định một thuật toán: đổi thuật toán là đổi mọi thứ tự đã lưu ngầm theo seed.
Lấy mẫu bằng Math.random() < 1/32 ở mỗi tầng? Mỗi tầng quyết định riêng: client giữ trace, backend bỏ, ra trace thiếu mảnh. Quyết định dựa trên ID thì nhất quán; OpenTelemetry làm đúng như vậy với TraceIdRatioBased sampler.
#Edge case và cạm bẫy
Math.floor(random() * n)lệch nhẹ khinkhông chia hết số giá trị có thể củarandom(). Vớinnhỏ (vài nghìn) độ lệch không đáng kể (~n/2³²); vớincỡ tỉ thì dùng phép từ chối (rejection sampling).- Đổi danh sách thì thứ tự đổi hết. Seed cố định nhưng thêm một câu hỏi vào đề là toàn bộ thứ tự xáo khác đi. Muốn ổn định từng phần tử, xếp theo
hash(seed + item.id)thay vì Fisher-Yates. - Seed từ dữ liệu người dùng (userId) thì ai biết userId cũng tính được nhóm hoặc thứ tự. Không sao với A/B; không được dùng cho thứ cần bí mật (thứ tự đáp án trong bài thi có thưởng).
- Số thực khác nhau giữa engine? Alea chỉ dùng phép nhân, cộng, trừ,
>>> 0,| 0trên số double theo IEEE 754, nên mọi trình duyệt và Node cho cùng một dãy. Đừng "tối ưu" bằngMath.froundhay đổi thứ tự phép tính. - Bộ sinh dùng chung giữa các tính năng. Hai tính năng cùng gọi một
random()thì thứ tự gọi quyết định kết quả; thêm một lời gọi ở tính năng A làm đổi kết quả của B. Mỗi việc một bộ sinh, seed riêng.
#Dùng trong dự án
import { alea, bucketOf, randomSeed, shuffleSeeded } from './seededRandom';
// Đề thi: mỗi thí sinh một thứ tự, ổn định khi tải lại
const ordered = shuffleSeeded(questions, exam.id, user.id);
// A/B: 10% người dùng thấy giao diện mới
const inTreatment = bucketOf('new-checkout-2026', user.id, 100) < 10;
// Test/demo tái lập: in seed ra để chạy lại
const seed = new URLSearchParams(location.search).get('seed') ?? randomSeed();
console.info('seed =', seed);
const random = alea(seed);
ts
#Kiểm thử
Ví dụ test (bản phác, không có trong repo)
import { describe, expect, it } from 'vitest';
import { alea, createTraceIdGenerator, shuffleSeeded, traceIdToBucket } from './seededRandom';
describe('alea', () => {
it('cùng seed cùng dãy, thứ tự seed có ý nghĩa', () => {
const a = alea('x', 'y'), b = alea('x', 'y'), c = alea('y', 'x');
const first = a();
expect(b()).toBe(first);
expect(c()).not.toBe(first);
});
it('shuffle không sửa mảng gốc và ổn định theo seed', () => {
const input = [1, 2, 3, 4, 5, 6];
const once = shuffleSeeded(input, 'quiz', 'u1');
expect(input).toEqual([1, 2, 3, 4, 5, 6]);
expect(shuffleSeeded(input, 'quiz', 'u1')).toEqual(once);
expect([...once].sort()).toEqual(input);
});
it('ID trace 11 ký tự, ký tự đầu A–H, nhóm lấy mẫu ~1/32', () => {
const next = createTraceIdGenerator(alea('trace'));
let zero = 0;
for (let i = 0; i < 64_000; i++) {
const id = next();
expect(id).toMatch(/^[A-H][A-Za-z0-9+/]{10}$/);
if (traceIdToBucket(id) === 0) zero++;
}
expect(zero / 64_000).toBeCloseTo(1 / 32, 2);
});
});
ts
#So với thư viện và chuẩn web
- seedrandom (npm) có chính Alea cùng nhiều PRNG khác (xor4096, tychei), API
seedrandom('hello'). Nếu không muốn tự giữ code, dùng nó. - pure-rand (dùng trong fast-check) có xoroshiro128+, mersenne twister, API bất biến hợp cho property-based testing.
- Faker, fast-check, Playwright đều có tham số seed để test tái lập; nguyên tắc giống bài này: in seed khi test hỏng.
- Lấy mẫu trace: OpenTelemetry
TraceIdRatioBasedSamplerquyết định từ các bit của trace ID, W3C Trace Context mang cờsampledđể các dịch vụ sau tôn trọng. - TC39 có đề xuất "Seeded Pseudo-Random Numbers" (
Random.Seeded), có thể đưa PRNG có seed vào JavaScript chuẩn; tính tới 10/2026 vẫn chưa dùng được trong trình duyệt.
Bài liên quan: Học từ Facebook #26: ring buffer biết trace nào đã mất log, Học từ Facebook #63: cây tổng và rút thăm theo trọng số.
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
- #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 (bài này)
- #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

