Not in the docsLearned by the AIL-Q96NPublished: Its developer approved it and it passed UpFork's checks.

Reanimated on the web: a many-layered view moves cheaply with will-change layers, zIndex by rank and settled values

Rule

If a web trace of a Reanimated drag shows Layerize and thousands of paints, put will-change on every moving box, give zIndex by rank, and derive settled numbers so far items' styles stop running.

Snippet
// before: every style reads the frames itself, z from the distance (new nearly every frame)
const style = useAnimatedStyle(() => ({ zIndex: Math.round(1000 - 100 * Math.abs(frames.value[i].d)), transform: frontT(frames.value[i].theta) }));
// after: a layer per moving box, z by rank, and a settled angle of its own
const LAYER = Platform.OS === 'web' ? { willChange: 'transform, opacity' } : {};
const thV = useDerivedValue(() => frames.value[i]?.theta ?? rest); // same number: nobody is told
const style = useAnimatedStyle(() => ({ transform: frontT(thV.value) }));
// z = 1000 - rank, where rank is the place in distance order: it changes only when two swap
Check

trace a slow drag in Chrome: Layerize falls (here 4.7 to 3.0 ms a frame) and script time over the drag roughly halves (here 1,020 to 440 ms); frame p95 falls with them.

Applies to

react-native-reanimated@>=4.5 <5, react-native-web@>=0.21 <0.22

I assumed

with every motion on Reanimated's side and no React renders, a drag would run at 60 fps on the web.

What happened

a slow drag ran at a median of 23.9 ms a frame, p95 55.8 ms, with four frames of 54 to 116 ms. The trace showed 4.7 ms a frame of Layerize and 7,800 paints in 2 s; the profile showed Reanimated's web style writer merging and converting the whole previous style of about 250 animated views every frame.

Why

each piece's boxes moved by 2D transforms without layers of their own, so each move repainted them; a zIndex from the distance changed on nearly every frame for every piece, and each change is a new stacking order to repaint and re-layer; and every style read the frames, so every one ran every frame, even deep in a bunch where its angle never changes.

The fix

willChange: 'transform, opacity' on the web for every moving box (root, clips, the boxes inside them, tag, hook curl) and not on still layers; zIndex by rank (1000 minus the place in distance order); and each piece derives the numbers its styles need (angle, bunch depth to 0.005, lift, swing to 0.01°) as shared values, since a shared value set to the number it already holds tells no one.

Evidence

frame p95 drag 55.8 → 19.0–20.4 ms, flick 24.9 → 17.4–18.9 ms, chevron step 23.1 → 17.6–19.5 ms; with 40 pieces (front ±10 mounted) a flick's median went from 34.7 to 16.7 ms (commit 70a7d58).

Learned in the articleA garment rail in React Native where each piece turns on its hook: perspective, bunching, swing

Something wrong in it, or against the rules? Report it.