Học từ mã nguồn Facebook: cách đọc code minify (mở đầu series)
Hệ module __d/require, gatekeeper, requireCond, JSResource của facebook.com và cách giải mã code minify từng bước. Mở đầu series 105 kỹ thuật frontend.
Facebook là một trong những ứng dụng web lớn nhất thế giới, chạy hàng triệu dòng JavaScript trên đủ loại thiết bị và mạng. Họ không open-source frontend của facebook.com, nhưng bundle JavaScript gửi xuống trình duyệt thì ai cũng tải về đọc được. Repo facebook-codebase chứa hơn 1.600 file như vậy, tải từ bản "Comet" (bản React hiện tại của facebook.com). Để tìm cho đủ, mình đã lập chỉ mục mọi lời gọi __d(...) trong repo: khoảng 25.000 khai báo module trong 1.451 file, tương ứng hơn 17.000 tên module khác nhau (một module thường có mặt ở nhiều bundle).
Series này đi qua những module mình thấy hay nhất và dễ mang sang dự án khác nhất. Mỗi bài có 4 phần:
- Facebook giải quyết vấn đề gì và giải quyết thế nào, kèm phân tích code gốc.
- Code viết lại bằng React 19 + TypeScript, gọn và có comment.
- Demo chạy ngay trong bài: bấm, cuộn, hover thử luôn.
- Bên trong trình duyệt, các bẫy, edge case và cách kiểm thử khi đưa vào production.
Tip
Tóm tắt
- Bundle JS của facebook.com đọc được, và chứa rất nhiều kỹ thuật frontend đáng học.
- Đọc được nó chỉ cần nắm vài quy ước:
__d(define),b("X")(require), 6 tham số factory, gatekeepergkx,cr:vàJSResource. - Series viết lại 105 kỹ thuật bằng React 19 + TypeScript, chia 15 phần, mỗi bài có demo chạy ngay trong trang. Năm phần cuối đi vào kết nối realtime, bên trong Relay, trình phát video, Messenger và văn bản, canvas/bản đồ và các bug kinh điển.
- Nhiều bài chỉ ra lỗi thật trong code gốc (tên sự kiện gõ sai, regex lọt khoá, bộ rút hàng đợi bỏ đói trình duyệt), đã kiểm chứng bằng cách chạy lại.
- Các đoạn "Kiểm thử" trong bài là bản phác để bạn tham khảo, không nằm trong repo ví dụ.
Warning
Mã nguồn của facebook.com thuộc bản quyền của Meta và không có license cho phép tái sử dụng. Đừng copy nguyên văn vào dự án. Toàn bộ code trong series này được viết lại từ đầu dựa trên ý tưởng đọc được. Những đoạn code gốc trích trong bài chỉ dài vài dòng để phân tích. Những phần Meta đã open-source (React, StyleX, Lexical, fbt) thì nên dùng bản chính thức.
Trước khi vào từng kỹ thuật, ta cần hiểu cách "đọc" đống code này. Phần dưới đây dài, nhưng đọc một lần thì cả 105 bài sau sẽ dễ đọc hơn nhiều.
#Hệ module của facebook.com
Facebook không dùng ES module (import/export) hay webpack runtime ở production. Họ có hệ module riêng, đi kèm một runtime nhỏ được nạp đầu tiên trong trang (trong repo là file 000/entry.js). Mỗi file JS gửi xuống là một chuỗi lời gọi __d(...):
__d("useDelayedState", ["React", "clearTimeout", "emptyFunction", "setTimeout"], (function (a, b, c, d, e, f) {
"use strict";
e.exports = a;
var g = b("React");
function a(a) { /* ... */ }
}), null);
js
#__d chỉ đăng ký, chưa chạy
__d(id, deps, factory, flags) viết tắt của define. Đọc runtime trong 000/entry.js, ta thấy nó chỉ lưu một bản ghi gồm id, danh sách dependency và hàm factory vào một map toàn cục. Factory chưa chạy. Factory chỉ chạy ở lần đầu tiên có ai đó require(id). Kết quả được giữ lại trong module.exports, lần require sau trả về luôn.
Hệ quả thực tế: tải về 2MB JS không có nghĩa là phải thực thi 2MB JS lúc khởi động. Phần đắt nhất của JavaScript trên điện thoại yếu thường là parse và execute, không phải tải. Module nào chưa cần thì chỉ tốn chi phí parse cú pháp, chưa tốn chi phí chạy.
Vài chi tiết khác đọc được từ runtime:
requirecủa module chưa từng chạy sẽ tự bọc trongErrorGuard.applyWithGuard(xem bài #6). Factory nào throw thì module bị đánh dấuhasError, các lầnrequiresau ném lại lỗiRequiring module "X" which threw an exception. Lỗi không bị nuốt.- Mỗi module có
refcount. Khi mọi nơi cần nó đãrequirexong, bản ghi trong map bị xoá cho nhẹ bộ nhớ. Ai cần thì đã giữexportsrồi. - Khi bật cờ
profile_require_factories, runtime đofactoryStart/factoryEndcủa từng module. Đây là cách họ biết module nào làm chậm lúc khởi động. - Nếu
exportslà function chưa códisplayName, runtime gắn thêm"(tên hàm) [from ModuleName]". Nhờ vậy React DevTools vẫn hiện tên dễ đọc dù code đã minify.
#6 tham số (a, b, c, d, e, f)
Factory luôn nhận 6 tham số chuẩn theo đúng thứ tự runtime truyền vào:
| Tham số | Là gì | Dùng thế nào |
|---|---|---|
a |
global (window) |
hiếm khi dùng, thường bị minifier tái sử dụng làm biến khác |
b |
require |
b("React") |
c |
requireDynamic |
gần như không dùng |
d |
requireLazy |
d(["X"], cb): chờ module X sẵn sàng rồi gọi cb |
e |
module |
e.exports = ... |
f |
exports |
f.isOnline = ... |
Nếu factory khai báo nhiều hơn 6 tham số, runtime truyền thêm exports của các dependency theo đúng thứ tự trong mảng deps. Ví dụ module gkx có deps ["invariant", "BanzaiLazyQueue", "emptyFunction"] và factory (a, b, c, d, e, f, g). Lúc đó g chính là invariant, nên trong code bạn sẽ thấy c != null || g(0, 3444, a). Đó là invariant(c != null, ...) với mã lỗi số thay cho message, vì message bị bỏ khỏi bản production để giảm dung lượng.
Tip
Minifier tái sử dụng tên tham số rất mạnh tay. a vừa là global, vừa là tên hàm được export (e.exports = a; function a() {} nhờ hoisting), vừa là tham số đầu tiên của hàm đó. Khi đọc, đừng tin tên biến. Hãy xem nó được gán ở dòng nào gần nhất.
#Bảng tra nhanh khi đọc
| Thấy gì | Nghĩa là |
|---|---|
!0 / !1 / void 0 |
true / false / undefined |
a === void 0 && (a = 0) |
tham số mặc định a = 0 |
x != null ? x : y |
x ?? y |
(h \|\| (h = b("X"))) |
lazy require: chỉ require X ở lần dùng đầu tiên |
babelHelpers["extends"]({}, a, b) |
{ ...a, ...b } |
babelHelpers.objectWithoutPropertiesLoose(a, ["children"]) |
const { children, ...rest } = a |
this.$1, this.$2, c.$6 = function |
private field / method của class, đã bị đổi tên theo số thứ tự |
g.jsx("div", {...}) |
JSX đã biên dịch: <div {...} /> |
g._("...") |
fbt: chuỗi đã dịch (i18n) |
#Gatekeeper: gkx("1475760")
Facebook bật/tắt tính năng theo người dùng bằng gatekeeper (GK). Đọc module gkx, ta thấy:
- Server nhúng sẵn kết quả của các GK mà trang cần vào HTML (file
gkxDatatrong repo), dạng{ "1475760": { result: false, hash: "AT7..." } }. Client không tự tính điều kiện, chỉ đọc kết quả. - Lần đầu một GK được đọc,
gkxgửi một eventgk2_exposure(quaBanzaiLazyQueue, xem bài #7). Mỗi GK chỉ gửi một lần. Đây là exposure logging cho A/B test: chỉ người dùng thực sự chạm tới đoạn code có GK mới được tính vào thí nghiệm, nên kết quả thí nghiệm không bị loãng.
Bài học mang đi được: feature flag nên log exposure ở thời điểm đọc flag, không phải lúc tải trang.
#requireCond và cr:1234: chọn module lúc build
Bạn sẽ thấy những dependency kỳ lạ như "cr:1142206" và b("cr:1142206"). Đây là kết quả của requireCond: chọn module ở bước build/serve, tuỳ điều kiện (bản dev hay prod, đang trong thí nghiệm nào…). Module requireCond gốc trong runtime chỉ có một dòng throw new Error("Cannot use raw untransformed requireCond."), nghĩa là mọi lời gọi đều phải được compiler biến đổi trước.
Ví dụ trong CometInfiniteScrollSuspenseList, b("cr:1142206") là một công cụ debug cho việc tải thêm ở cuối danh sách. Code luôn viết b("cr:1142206") != null && b("cr:1142206").log(...), nghĩa là điều kiện có thể chọn null cho slot đó. Với người dùng thường, module debug có thể không hề có mặt trong bundle. Với nhân viên nội bộ, slot đó là module thật. Đây là phiên bản "build-time" của if (process.env.NODE_ENV !== 'production') mà ta quen dùng.
#JSResource: code splitting kiểu Facebook
React.lazy(() => import("./X")) là cách ta code-split. Ở Facebook là JSResource("X"):
-
JSResource("X")trả về một reference (JSResourceReference), được cache theo tên module. Gọi bao nhiêu lần cũng ra cùng một object. -
.preload()báoBootloadertải trước, chưa chạy..load()trả về Promise của module. -
Có hai hàm lấy module đồng bộ, trả về
nullnếu chưa được:getModuleIfRequireable(): code đã tải về (module đã được__d) thìrequireluôn và trả về, kể cả khi factory chưa từng chạy.getModuleIfRequired(): chỉ trả về nếu module đã được require ở đâu đó rồi.
Nhờ hai hàm này, component có thể render đồng bộ khi code đã có sẵn thay vì suspend.
BootloaderResource.readdùnggetModuleIfRequireable, và ta sẽ gặp lại đúng ý tưởng này trong bài #3. -
Bootloaderbiết mỗi module nằm trong file JS/CSS nào (qua "resource map" server gửi xuống), gom nhiều module vào ít request, tải CSS đi kèm và theo dõi lỗi tải.
flowchart LR
A[HTML từ server] --> B[runtime entry.js]
A --> C[gkxData + resource map]
B --> D[__d đăng ký factory]
D --> E{require lần đầu?}
E -- có --> F[chạy factory, lưu exports]
E -- không --> G[trả exports đã cache]
H[JSResource.preload] --> I[Bootloader tải file JS]
I --> D
#Giải mã một module từ đầu tới cuối
Lấy module nhỏ useDelayedState (thư mục hooks/ của repo) làm ví dụ. Phần lõi trông như sau:
a = g.useCallback(function (a, c, f) {
c === void 0 && (c = 0), f === void 0 && (f = b("emptyFunction")),
b("clearTimeout")(e.current), e.current = null,
c === 0 ? (d(a), f(a)) : e.current = b("setTimeout")(function () {
d(a), f(a), e.current = null
}, c)
}, []);
return [c, a]
js
Bước 1: tìm xem biến nào là gì. Phía trên đoạn này có a = g.useState(a); var c = a[0], d = a[1], e = g.useRef(null);. Vậy c là state, d là setState, e là ref giữ timer. Trong useCallback, tham số a, c, f che các biến bên ngoài: a là giá trị mới, c là độ trễ, f là callback.
Bước 2: tách các biểu thức nối bằng dấu phẩy thành từng câu lệnh, và đổi x === void 0 && (x = ...) thành tham số mặc định.
Bước 3: đổi cond ? (A, B) : C thành if/else.
Bước 4: đặt tên và thêm type:
function useDelayedState<T>(initial: T) {
const [state, setState] = useState(initial);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// unmount thì huỷ timer đang chờ
useEffect(() => () => clearTimeout(timerRef.current ?? undefined), []);
const setDelayedState = useCallback(
(value: T, delay = 0, callback: (value: T) => void = () => {}) => {
clearTimeout(timerRef.current ?? undefined); // lần set mới huỷ lần đang chờ
timerRef.current = null;
if (delay === 0) {
setState(value);
callback(value);
} else {
timerRef.current = setTimeout(() => {
setState(value);
callback(value);
timerRef.current = null;
}, delay);
}
},
[],
);
return [state, setDelayedState] as const;
}
ts
Đọc ra rồi thì thấy đây là một hook rất hữu ích: setOpen(true, 300) để tooltip chỉ hiện khi hover đủ lâu, setOpen(false) để tắt ngay và huỷ luôn lần hiện đang chờ. Đây chính là cơ chế chống "nháy" tooltip khi rê chuột qua nhanh.
Mẹo đọc nhanh cho mọi module:
- Đọc danh sách dependency trước.
["CometMouseActivity", "JSScheduler", "setTimeout"]đã cho biết module này làm gì với chuột, có lập lịch, có hẹn giờ. - Tìm chuỗi. Tên lỗi, tên event, tên log (
"Network switched to offline","COMPONENT_UNMOUNTED") là manh mối tốt nhất. - Đọc theo cụm. Một tính năng thường gồm 5–15 module có tên giống nhau (
CometToaster*,Error*). Đọc cả cụm dễ hiểu hơn đọc lẻ từng file. - Kiểm chứng bằng cách viết lại và chạy thử. Đó là cách mình làm cho cả series này.
#105 kỹ thuật trong series
Series chia làm mười lăm phần. Phần 1 (#1–#8) tập trung vào hạ tầng chạy ngầm: observer, logging, lỗi, mạng, focus. Phần 2 (#9–#20) đi sang những thứ người dùng nhìn thấy: mẹo CSS không cần JS, thanh cuộn, modal, popover, tab, khung chat, bố cục ảnh. Cuối phần 2 là các kỹ thuật giữ cho một SPA chạy lâu vẫn nhanh và ổn định. Phần 3 (#21–#37) là đợt đọc lại toàn bộ bundle, từng thư mục và từng file: Smart Compose, MQTT ngay trong trình duyệt, đo thời gian dùng, engine animation, bootloader, xử lý ảnh trong worker, kéo-thả và các module hạ tầng nhỏ. Ba phần tiếp theo đi sâu thêm một lượt nữa: Phần 4 (#38–#42) là Messenger và realtime (đồng bộ tin nhắn theo seqID, ID sinh ở client, đo thời gian gửi tin, bộ điều phối request); Phần 5 (#43–#50) là các module sản phẩm như composer, thanh toán, quảng cáo, Pages; Phần 6 (#51–#57) là hạ tầng sâu: phát hiện script lạ, localStorage đầy quota, cache tuyến, lưới phím 2D, tách emoji, gom đọc/ghi DOM, hàng đợi lệnh video.
Bốn phần sau cùng là lượt đọc thứ ba, dựa trên chỉ mục toàn bộ khai báo module. Phần 7 (#58–#62) là video và upload: ước lượng băng thông, chọn bitrate, prefetch segment DASH, đọc metadata MP4 và upload tiếp tục được. Phần 8 (#63–#70) là thuật toán thuần: Fenwick tree, vạch trục "đẹp", khoảng cách sửa chữ, PRNG có seed, heap đôi, memo nhiều khoá, thống kê cửa sổ trượt, số 64 bit và UTF. Phần 9 (#71–#74) là thời gian, chuỗi và URL: múi giờ IANA ngay trên client, semver, auto-link và parser query kiểu PHP. Phần 10 (#75–#82) là hạ tầng, bảo mật và hình học: popover tự lật, tiền tố chống JSON hijacking, chống lộ referrer, setImmediate tự viết, phát hiện tab crash, cookie DPR và che khoá trong log, Flux waitFor, crop và xoay sticker.
Năm phần cuối là lượt đọc thứ tư, quét tiếp khoảng 1.900 module còn lại. Phần 11 (#83–#86) là kết nối và realtime: các tab chia nhau subdomain bằng lease, MQTT QoS1 gửi lại khi kết nối lại, cache Promise và lịch retry. Phần 12 (#87–#91) mở bên trong Relay: giữ tham chiếu nhánh không đổi, store optimistic, chỉ mục subscription, phân trang connection và profiler kiểu middleware. Phần 13 (#92–#95) là trình phát video: đọc EBML/WebM, stream Range tạm dừng được, bảng khoảng thời gian gắn nhãn, SourceBuffer và đồng bộ video với animation. Phần 14 (#96–#100) là Messenger và văn bản: gom tin nhắn thành cụm, chính sách mở tab chat, phím tắt Markdown, offset code point và typeahead không nhảy. Phần 15 (#101–#105) là canvas, bản đồ, dò tính năng trình duyệt và các bug kinh điển về locale và dispose.
| # | Kỹ thuật | Mang về được gì |
|---|---|---|
| 1 | Dùng chung IntersectionObserver | 1 observer cho hàng trăm element, infinite scroll không gọi API trùng |
| 2 | useViewportDuration | Đo thời gian người dùng thực sự xem một bài, kèm lý do ẩn |
| 3 | Preload khi hover | Tải trước code + data trước khi click, và bẫy 300ms của Suspense |
| 4 | Toast manager | Store ngoài React, hàng đợi, dừng đồng hồ khi hover/focus |
| 5 | Network heartbeat | Biết online/offline thật bằng heartbeat có backoff |
| 6 | ErrorGuard | Bắt lỗi kèm ngữ cảnh, một pipeline cho mọi loại lỗi |
| 7 | Batch logger (Banzai) | Gom log theo lô, ưu tiên, gửi bằng sendBeacon khi rời trang |
| 8 | Focus trap | Giữ focus trong dialog, Esc chỉ đóng lớp trên cùng |
| 9 | Mẹo calc(… * 9999) |
Style theo kích thước container chỉ bằng CSS |
| 10 | Thanh cuộn CSS 3D | Thanh cuộn tuỳ biến chạy trên compositor |
| 11 | Modal cuộn bằng window | Mở/đóng modal mà trang nền giữ đúng vị trí cuộn |
| 12 | Popover, tooltip, hovercard | Định vị, tự lật, tooltip nhóm, hovercard xuyên portal |
| 13 | Pressable | Primitive nút/link: press-out, focus-visible, Ctrl+click |
| 14 | Tab "Xem thêm" + sticky header | Gom tab thừa và biết header đang dính mà không đo bằng JS |
| 15 | Mẹo hình ảnh | Avatar khoét mask, gradient chat, skeleton tiết kiệm CPU |
| 16 | Khung chat không giật | Dính đáy, giữ chỗ khi tải tin cũ |
| 17 | Bố cục collage ảnh | Chấm điểm bố cục theo độ cắt xén, cắt theo focal point |
| 18 | Keep-alive tab | Quay lại tab là thấy ngay nguyên trạng thái |
| 19 | Visual Completion | Tự đo Speed Index và thời điểm tương tác render xong |
| 20 | Router SPA bền bỉ | Tự reload đúng lúc, tải JS/CSS có retry và ngắt mạch |
| 21 | Smart Compose | Gợi ý chữ mờ khi gõ, không làm phiền người gõ nhanh |
| 22 | MQTT trong trình duyệt | Codec nhị phân, tự chuyển sang long-poll khi WebSocket hỏng |
| 23 | Bitmap 64 giây | Đo thời gian dùng thật bằng bitmap, lấy mẫu theo user |
| 24 | Reaction nhấn giữ rồi trượt | Chọn bằng cách trượt ngón tay, chặn click nhầm |
| 25 | Tìm kiếm gấp dấu | Gấp dấu, phiên âm, highlight đầu từ an toàn |
| 26 | Ring buffer cho trace | Biết trace nào đã mất log khi buffer tràn |
| 27 | Engine animation JS | Một vòng rAF dùng chung, tự giải cubic-bezier |
| 28 | Bootloader | Bitmap tài nguyên đã tải, chỉ reload khi module đang dùng đổi |
| 29 | Click của screen reader | Nhận biết click do trình đọc màn hình tạo ra |
| 30 | Prefetch theo chất lượng mạng | Mạng càng kém càng tải trước xa, đoán hạng thiết bị |
| 31 | Hàng đợi gửi tin | Giữ đúng thứ tự tin nhắn khi mạng chập chờn |
| 32 | ApiBatcher | Gộp các lời gọi API trong cùng một tick |
| 33 | Resize ảnh trong pool worker | Thu nhỏ ảnh trước khi upload, worker chết thì tạo lại |
| 34 | Crop theo nhiều khuôn mặt | Cửa sổ crop giữ nhiều mặt nhất, đổi toạ độ khi xoay |
| 35 | Kéo-thả không nhấp nháy | Đổi chỗ theo mép, overlay thả file đếm enter/leave |
| 36 | Bốn tiện ích UI | Fade không kẹt, sự kiện dính, callback huỷ được, sentinel không kẹt |
| 37 | Cookie, hash lỗi, chuột dừng | Một bảng chính sách cho mọi cookie |
| 38 | Đồng bộ tin nhắn theo seqID | Phát hiện lỗ seqID, fetch bù theo từng tầng |
| 39 | OTID sinh ở client | ID tin nhắn kiểu Snowflake, tự sắp theo thời gian |
| 40 | QPL và RTT gửi tin | Lấy mẫu từ lúc bắt đầu, đo gửi tin đầu-cuối |
| 41 | Bộ điều phối request | Bảy chế độ gộp và chống trùng theo endpoint |
| 42 | Thread list và presence | Phân trang theo mốc thời gian, cookie có version |
| 43 | Composer dạng plugin | Plugin tự đăng ký reducer và bước kiểm tra trước khi đăng |
| 44 | Thanh toán bất đồng bộ | Subscribe trước khi charge, 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 | Khoảng qua đêm, gộp khoảng, lấy phần bù |
| 48 | Tiến độ đăng realtime | Giữ event chờ snapshot, watchdog chuyển sang REST |
| 49 | Tiền tệ theo đơn vị nhỏ nhất | Định dạng và ô nhập tiền không làm tròn sai |
| 50 | Sửa hàng loạt, seen/read 2 bit | Empty/Uniform/Mixed, XOR để biết bit nào đổi |
| 51 | Phát hiện hook hàm native | Biết script lạ nào đã bọc API trình duyệt |
| 52 | localStorage đầy quota | Tự dọn nửa entry cũ nhất rồi ghi lại |
| 53 | Cache tuyến | Server phân giải route, client chờ sẵn promise |
| 54 | Lưới phím mũi tên 2D | Ô gộp cột, RTL, roving tabindex, focus không cuộn |
| 55 | Tách cụm emoji | Máy trạng thái so với Intl.Segmenter |
| 56 | Đọc trước ghi sau | Tránh layout thrashing, con tự biết vị trí |
| 57 | Hàng đợi lệnh video | Nén play/pause/seek, stream sắp lại chunk |
| 58 | Ước lượng băng thông EWMA | Loại outlier, nhớ băng thông qua phiên |
| 59 | ABR theo độ tự tin | Chọn bitrate theo độ tự tin phát, cắt bậc theo MOS |
| 60 | Segment DASH dự đoán | Prefetch segment từ manifest, parse hộp sidx |
| 61 | Metadata MP4/MOV | Đọc thông tin video ngay trên client, trước khi upload |
| 62 | Upload tiếp tục được | Offset, hash chống trùng, phát hiện treo |
| 63 | Fenwick tree | Rút thăm có trọng số và offset dòng trong O(log n) |
| 64 | Vạch trục "đẹp" | Bước 1/2/5 × 10^k, vạch thời gian theo đơn vị lịch |
| 65 | Damerau-Levenshtein | Khoảng cách sửa chữ theo dải chéo, tự sửa tên gõ sai |
| 66 | PRNG có seed | Shuffle tái lập được, lấy mẫu trace ổn định |
| 67 | Heap đôi | Tái dùng vị trí DOM trong danh sách ảo |
| 68 | Memo nhiều khoá | Map/WeakMap lồng nhau, interning object |
| 69 | Thống kê cửa sổ trượt | Trung bình động, đại số khoảng thời gian |
| 70 | Int64 và UTF | Số 64 bit từ hai số 32 bit, offset theo code point |
| 71 | Múi giờ IANA trên client | Engine múi giờ tự viết, phát hiện đồng hồ lệch |
| 72 | Semver range | Tự cài so khớp phiên bản ^, ~, khoảng |
| 73 | Auto-link, cắt chữ an toàn | Không chặt đôi URL, giới hạn LaTeX lồng nhau |
| 74 | Parser query kiểu PHP | a[b][]=1 mà không bị prototype pollution |
| 75 | Popover tự lật | Chọn hướng có phần tràn nhỏ nhất, mũi tên theo RTL |
| 76 | Tiền tố for(;;) |
Chống JSON hijacking, parse phản hồi streaming |
| 77 | Referrer và linkshim | Không lộ URL riêng tư, xoá token khỏi thanh địa chỉ |
| 78 | setImmediate tự viết | Né mức kẹp 4ms, gom delta chuột theo frame |
| 79 | Phát hiện tab crash | Heartbeat localStorage, chỉ một tab báo mỗi crash |
| 80 | Cookie DPR, che khoá trong log | Ảnh @2x từ lượt tải sau, regex khoá SDES đúng |
| 81 | Flux waitFor, proxy có đệm | Thứ tự store, lỗi vòng kèm chuỗi, gọi trước khi tải |
| 82 | Crop và xoay sticker | Crop giữa theo tỉ lệ, góc sticker trong vùng an toàn |
| 83 | Lease subdomain giữa các tab | Né giới hạn 6 kết nối mỗi host, long-poll theo bảng trạng thái |
| 84 | MQTT QoS1 gửi lại | Giữ publish chờ ack, gửi lại khi kết nối lại |
| 85 | Cache Promise | Gộp request trùng, 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ữ tham chiếu nhánh không đổi | Component con khỏi render lại khi dữ liệu y nguyên |
| 88 | Store optimistic | Copy-on-write, rollback rồi áp lại update còn sống |
| 89 | Chỉ mục subscription | Chỉ báo cho subscriber dính record bị đổi |
| 90 | Phân trang connection | Cursor phải khớp đầu danh sách, edge không trùng |
| 91 | Profiler kiểu middleware | Bọc hàm theo chuỗi, kiểm selector có thật sự thuần |
| 92 | Đọc EBML/WebM | Tìm byte range segment từ Cues, số nguyên variable-length |
| 93 | Stream Range tạm dừng được | Ghép segment liền kề, tải tiếp đúng byte |
| 94 | Khoảng thời gian gắn nhãn | Ghi đè khoảng, nhảy qua lỗ buffer |
| 95 | SourceBuffer và đồng bộ video | Gom chunk trước khi append, chỉnh playbackRate cho khớp |
| 96 | Gom tin nhắn thành cụm | Cụm 5 phút, vạch ngày, avatar đã xem |
| 97 | Chính sách tab chat | Số tab theo chiều rộng, chuỗi luật mở tab |
| 98 | Phím tắt Markdown | Gõ - thành list, Backspace để hoàn tác |
| 99 | Offset code point | Bốn bug cùng gốc UTF-16 và cách tránh |
| 100 | Typeahead không nhảy | Cache tiền tố, chặn reflow, khoá hover khi cuộn phím |
| 101 | Hit-test trên canvas | Phát lại path để biết chuột trúng hình nào |
| 102 | Vòng tròn theo mét | Đổi bán kính mét sang ellipse pixel Mercator |
| 103 | Dò tính năng trình duyệt | Ảnh EXIF 2×1, getter bẫy, đo scrollbar |
| 104 | Bug locale và validate | Số theo locale, regex, thứ tự toán tử |
| 105 | Dispose cho đúng | Huỷ idle callback, splice(-1), xoá đúng key |
Tất cả demo được gom trong một project Vite nhỏ: examples/facebook-patterns. Bạn có thể clone về chạy npm install && npm run dev, hoặc mở trên StackBlitz để sửa code ngay trên trình duyệt.
▶ Mở demo toàn màn hình ↗ · Sửa code trên StackBlitz ↗
#Điểm chung của các kỹ thuật
Đọc xong cả series, bạn sẽ thấy vài nguyên tắc Facebook lặp đi lặp lại:
- Tách state khỏi React. Toast, network status, logger đều là object/class thuần với
subscribe+getSnapshot. React chỉ là một "view" cắm vào quauseSyncExternalStore. Nhờ vậy code nào cũng gọi được (toast.push()trong một hàm fetch chẳng hạn) và test được mà không cần render. - Dùng chung tài nguyên đắt. Một IntersectionObserver cho hàng trăm element, một listener
keydowncho mọi layer, một hàng đợi cho mọi log, một bản ghi module cho mọi lầnrequire. - Làm muộn nhất có thể. Factory chỉ chạy khi cần, observer chỉ được tạo khi có người observe, listener chỉ gắn khi có người subscribe.
- Không tin trình duyệt 100%.
navigator.onLinecó thể sai.isIntersectingchưa có nghĩa là người dùng đang xem.unloadthường không chạy trên mobile. - Đo trước, tối ưu sau. Rất nhiều module chỉ để log: thời gian chạy factory, exposure của gatekeeper, thời gian hiển thị, guard stack lúc lỗi.
- Tận dụng thời gian chờ của người dùng. Từ lúc đưa chuột lên nút tới lúc click thường mất vài trăm mili-giây. Đó là khoảng thời gian miễn phí để tải trước.
Bắt đầu với bài đầu tiên: Shared IntersectionObserver.
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 (bài này)
- #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
- #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

