Web Totals

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.

hunghg255hunghg25530 min read
#facebook#react#javascript#frontend#series
Học từ mã nguồn Facebook: cách đọc code minify (mở đầu series)

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:

  1. 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.
  2. Code viết lại bằng React 19 + TypeScript, gọn và có comment.
  3. Demo chạy ngay trong bài: bấm, cuộn, hover thử luôn.
  4. 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, gatekeeper gkx, 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:

  • require của module chưa từng chạy sẽ tự bọc trong ErrorGuard.applyWithGuard (xem bài #6). Factory nào throw thì module bị đánh dấu hasError, các lần require sau ném lại lỗi Requiring 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ó đã require xong, bản ghi trong map bị xoá cho nhẹ bộ nhớ. Ai cần thì đã giữ exports rồi.
  • Khi bật cờ profile_require_factories, runtime đo factoryStart/factoryEnd củ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 exports là 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 gkxData trong 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, gkx gửi một event gk2_exposure (qua BanzaiLazyQueue, 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áo Bootloader tả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ề null nếu chưa được:

    • getModuleIfRequireable(): code đã tải về (module đã được __d) thì require luô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.read dùng getModuleIfRequireable, và ta sẽ gặp lại đúng ý tưởng này trong bài #3.

  • Bootloader biế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.

#Đ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 qua useSyncExternalStore. 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 keydown cho mọi layer, một hàng đợi cho mọi log, một bản ghi module cho mọi lần require.
  • 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.onLine có thể sai. isIntersecting chưa có nghĩa là người dùng đang xem. unload thườ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

Phần 2

Phần 3

Phần 4–6

Phần 7–10

Phần 11–15

Code + demo: examples/facebook-patterns · Mở trên StackBlitz