Facebook Comet Prelude: ServerJS and the data-sjs Pipeline: How Facebook Streams Its UI
# ServerJS and the data-sjs Pipeline: How Facebook Streams Its UI
*Part 5 of The Facebook Comet Prelude Files · frankSx · frankhacks.blogspot.com*
---
Facebook pages are not rendered. They are **streamed**. The HTML you receive is a skeleton plus a series of `
```
The payload keys the prelude understands, from the modules that consume them:
| Key | Consumer | Meaning |
|---|---|---|
| `define` | `ServerJSDefine` | Register `__d` modules server-side |
| `require` | `ServerJS.handle` | Require-with-args instructions (conditional via `__cond_*` handles) |
| `hsrp` / `hblp` | `HasteResponse` / Bootloader | Haste resource map — which CSS/JS files exist and their metadata |
| `qplTimingsServerJS` | `qplTimingsServerJS` module | Server-side perf timings to merge into client traces |
| `qplAnnotationsIntServerJS` / `qplAnnotationsStringServerJS` / `qplTagServerJS` | QPL | Trace annotations and tags |
| `ix` | `ix` module | Static asset (image) manifest entries |
| `bx` | `bx` module | Bloks (server-driven UI) asset manifest entries |
| `cx` | `cx` module | CSS class-name obfuscation map entries |
`ix`/`bx`/`cx` are worth a sentence each: they're the three "manifest" channels by which the server ships lookup tables (images, Bloks assets, CSS names) that later modules consume by opaque key — which is why Facebook's markup is full of meaningless `x1a2b3c` class names that remap between deploys.
## 2. The listener: two generations, side by side
The prelude ships **two** payload listeners, selected by `window.Env.sjsListenerNew`:
```js
// de-minified from ServerJSPayloadListener (legacy path)
function processPayload(scriptEl) {
if (!(scriptEl instanceof HTMLScriptElement)) return;
var declaredLen = scriptEl.dataset.contentLen;
if (scriptEl.dataset.processed) return;
if (scriptEl.textContent.length.toString() !== declaredLen) return; // integrity gate
scriptEl.dataset.processed = "1";
var payload = GHLServerJSParse.ghlParseServerJSPayload(scriptEl.textContent);
if (payload == null)
throw err("ServerJS payload marked with data-sjs was parsed as null");
new ServerJS().handle(payload);
}
function process() {
if (document == null) return;
var scripts = document.querySelectorAll("script[data-sjs]:not([data-processed])");
for (var el of scripts) processPayload(el);
}
```
The differences between the generations are exactly the interesting part:
1. **Legacy** parses through `GHLServerJSParse.ghlParseServerJSPayload` — the Ghost Owl defensive parser (Part 1) with shim detection and the json5 fallback.
2. **New** (`ServerJSPayloadListener_NEW`) uses plain `JSON.parse` — but only runs when `window.Env.sjsListenerNew` is set, i.e. when the server has decided this client/cohort doesn't need the GHL treatment.
Both share the **content-length integrity gate**: the server declares `data-content-len`, and the payload executes only if `textContent.length` matches exactly. This defeats in-transit injection and truncation by middleboxes — a payload modified by a proxy or extension fails the length check and is silently skipped. It stays unprocessed rather than erroring, so a later `process()` pass could still pick it up if the DOM is repaired.
Note the re-entrancy design: `process()` is idempotent (the `data-processed="1"` marker) and is meant to be called repeatedly as the streamed document grows — each pass scans the whole document but only touches new tags. There is no MutationObserver here; the streaming parser calls `process()` as chunks arrive.
## 3. Transport markers
The legacy parse path optionally pre-transforms the payload via `replaceTransportMarkers` (gate `k8pq2mnb`). That module:
- Maintains a `Set` of registered marker strings.
- Rewrites occurrences of each marker in the raw payload text before parsing.
- Queues an exposure log through `BanzaiLazyQueue` when markers actually fire.
Transport markers are how the server smuggles values that would be mangled by intermediaries (or by the GHL decoy/filter ecosystem) through the wire format: the server writes a placeholder, the client substitutes the real value post-receipt, pre-parse. It's a checksum-free integrity side-channel — and the exposure logging means Meta knows when intermediaries touched the payload.
## 4. CSRFGuard on the response side
Payloads arriving via XHR (bootloader endpoint, async navigations) rather than inline tags carry the `for (;;);` prefix, stripped by `CSRFGuard.clean` before the same ServerJS machinery handles them (Part 2, §5). One parser, two transports, two different injection defenses: length-gated inline tags, prefix-guarded XHR.
## 5. The execution model: scheduled, guarded, incremental
`ServerJS.handle` doesn't execute instructions synchronously. The prelude wraps it in scheduling layers:
- **`ScheduledServerJS`** — runs `ServerJS.handle` through `JSScheduler` via `ScheduledApplyEach`, so payload execution interleaves with rendering instead of blocking the main thread.
- **`ScheduledServerJSDefine`** — same treatment for `ServerJSDefine` registration.
- **`ScheduledServerJSWithCSS`** — coordinates ServerJS execution with CSS arrival through Bootloader, so markup doesn't execute before its styles exist (no FOUC-by-design).
- **`nowServerJS`** — immediate-execution escape hatch for critical instructions.
- **`ErrorGuard`** — every instruction runs guarded; a throwing `require` instruction is reported without aborting the rest of the payload.
- **`HasteBitMap`** — every executed define/require is recorded in the bitmap that later rides on async requests (Part 3, §5), closing the loop so the server never re-sends what already ran.
- **`ServerJsRuntimeEnvironment`** — carries per-payload runtime flags (e.g. whether usage stats are reported).
`ServerJSDefine` itself does more than register modules: it maintains the **loaded-module hash** (`getLoadedModuleHash()` — the `jsmod` parameter from Part 2) and handles duplicate-definition accounting (`dup_user_define` vs `dup_system_define` in the payload stats), distinguishing user-land redefines from system redefines.
## 6. Observable streaming behavior (for researchers)
Incremental execution has a side effect useful for analysis: **the page is functional in stages**, and each stage's capabilities tell you which payloads have arrived. Practical probes:
- `document.querySelectorAll("script[data-sjs]")` growing over time, correlated with `data-processed` markers, reveals the server's streaming strategy — what it prioritizes (chrome, then feed units, then below-fold), what it defers, and how it reacts to connection class (`ccg` from Part 2 feeds those decisions).
- `data-content-len` vs actual length is a free middlebox detector for your own connection.
- The `__cond_*` handles in `require` instructions resolve through `ContextualComponent`/`Parent` — the mechanism that binds streamed instructions to DOM containers without global selectors.
## 7. Why the design is worth studying
ServerJS is a 15-year-old answer to the problem the industry later re-solved as React Server Components and streaming SSR: ship *instructions*, not markup, and let the client assemble the UI as data arrives. The prelude implementation shows the production scars the new hotness will eventually grow: transport integrity checks, parser-distrust fallbacks, anti-tamper repair passes, scheduling to keep the main thread alive, manifest channels for every asset class, and a feedback loop (Haste bitmaps + loaded-module hash) so the server knows exactly what the client has executed.
---
*Next: the observation deck — Facebook's error, telemetry, and instrumentation stack, including the Hyperion interception engine.*
Comments
Post a Comment