TT Lab
Get started
Learn Learning paths Courses

SSR — The Server Draws First

A Hydration Differ

Continue in TT Lab

Goal

You build a differ that compares the tree the server drew with the tree the client drew, finds the mismatched spots, and turns them into how far up to the boundary you have to redraw.

Why it matters

Hydration attaches handlers to a DOM that already exists, so when the two sides draw different things the screen stays quietly wrong. That is why frameworks put effort into finding and reporting the mismatched spots, and for that report to be useful, two things are needed.

First, it must stop at the spot where the shape is off. If one item is inserted at the front of a list, everything below it shifts, and if you keep descending, hundreds of mismatches pour out and the real cause is buried. Second, it must know the boundaries. Even the same mismatch only needs its subtree redrawn if it is inside a boundary, but outside a boundary the whole page is redrawn.

The shape of the tree and the path

A node is one of two things.

In a path, the root is "$", and the i-th child is the parent path + "/" + i. For example: the first child of the root's second child is "$/1/0".

One mismatch is { path, kind }, and kind is one of text, attr, tag, and children. If it is attr, name additionally holds the attribute name.

Steps

  1. Text comparison and paths
  2. Attribute comparison — order is not a mismatch
  3. Stop when the shape is off
  4. Suppress only one level
  5. Attach the nearest boundary
  6. Compute where to redraw — repairPlan
  7. Measure it yourself and write it down — /root/work/hydrate/07-report.txt
  8. Wrap-up — /root/work/hydrate/08-notes.md

Notes

Text comparison and paths

In /root/work/hydrate/hydrate.mjs, export diff(server, client). It returns an array holding {path, kind} for each mismatched spot. If the text differs, kind is "text"; if only one side is text, it is "tag". For identical trees it is an empty array.

mkdir -p /root/work/hydrate. In a path, the root is "$" and the i-th child is the parent path with "/" + i appended. Carry the path along as an argument while you recurse. In this step you may assume the two trees have the same shape.

Attribute comparison — order is not a mismatch

Compare the props of element nodes. If a value differs or exists on only one side, record {path, kind: "attr", name}. The order of attributes is not a mismatch.

If you collect the keys from both sides into one set and then compare values by name, the order drops out naturally. An attribute present on only one side has the value undefined on the other side, so it is caught automatically. The browser does not tell attributes apart by order, so if you count order as a mismatch, a perfectly fine page turns red all over.

Stop when the shape is off

If the tags differ, record one {path, kind: "tag"} and do not descend below it. If the number of children differs, record one {path, kind: "children"} and stop there too.

If one item is inserted at the front of a list, everything below it shifts. If you keep descending, you end up comparing nodes that are not a pair, hundreds of mismatches pour out, and the one real cause is buried in the pile. The grader checks that point with a list shifted by one slot.

Suppress only one level

Accept an array of paths in options.suppress of diff(server, client, options). Leave out only the text and attr mismatches of that path itself, and keep reporting the paths below it. tag and children are not subject to suppression.

It must work as is even if the third argument is missing or an empty object. The reason for allowing only one level is the same as for React's suppressHydrationWarning — if you let one time value slide and close your eyes to everything below it, the real mismatches inside are never seen.

Attach the nearest boundary

Add a boundary to each mismatch. It is the value of the nearest boundary that wraps that path in the server tree, and null if nothing wraps it. If the node itself opens a boundary, that node's own mismatch also belongs to its own boundary.

If you walk the tree once and record the "boundary that is open right now" for each path, afterwards you only have to look it up. With nested boundaries, the inner one wins. Whether any null comes out is the report card of the design — that one makes the whole page get redrawn.

Compute where to redraw

Export repairPlan(mismatches). It returns {boundaries, full}. boundaries is an array of the boundary ids that must be redrawn, without duplicates and in the order they first appear, and full is true if there is even one mismatch outside a boundary.

Even if ten mismatches occur inside the same boundary, the redraw happens once. So the answer is a set, not a list, but it is better to preserve the order a person will read it in. Do not put a mismatch outside a boundary into boundaries; mark it only with full.

Measure it yourself and write it down

Feed the server and client of /opt/fixtures/ssr-hydrate/pair.json into your diff and repairPlan, and write the resulting values in /root/work/hydrate/07-report.txt as three lines of the form 이름=값 (the placeholders are the name and the value). mismatches — the number of mismatches. boundaries — the number of boundaries to redraw. full — yes if there was a mismatch outside a boundary, no if not.

Make a short .mjs, run it, and copy the values that come out. The grader compares the same file again with your differ and checks it against the written values, so do not estimate by eye. If you do not stop at a spot where the number of children differs, the number of mismatches goes up.

What contained the mismatches

In /root/work/hydrate/08-notes.md, write at least three lines. Cover what shows on screen when the server and the client drew different things, what a mismatch outside a boundary means, and why suppression is allowed for only one level.

The text must contain 불일치, 경계, and 억제 (the Korean words for "mismatch", "boundary", and "suppression"). These three are what teams that use SSR actually spend days on.