Ydeevne
Hvorfor en film koster det, den gør, den sande historie om Matrix-regn bygget af 1.800 div-elementer og et budget, du kan holde filmen op imod.
En film er hverken en shader eller en video. På hvert billede monterer en vært React-træet ved ét præcist billede, lader browseren beregne layout og tegne det og rasteriserer resultatet. En film koster derfor det, dens DOM koster at lægge ud og tegne — ganget med antallet af billeder, som for en film på ti sekunder ved 30 fps er tre hundrede.
Det gør ydeevnen usædvanligt forudsigelig. Der er ingen mystisk belastende løkke. Der er fire ting, der koster ressourcer.
Hvad tager tid
Antal elementer. Hvert element kræver layout og optegning på hvert billede. Et par hundrede er fint. Et par tusind er ikke, og udgiften forsvinder ikke, fordi elementerne er små eller uden for skærmen.
Layout. Et træ af absolut positionerede bokse er billigt at lægge ud. Dybe indlejringer, flex inde i flex inde i grid og tekst, der skal måles og ombrydes, er ikke. Film placerer mest ting ved kendte koordinater, hvilket er det billige tilfælde — hold dig til det.
Optegning, især sløring. Sløring er langt den dyreste optegning: filter: blur(), backdrop-filter og store radier for text-shadow eller box-shadow. Prisen vokser med radius og areal og betales én gang pr. element, der har dem. Ét sløret panel er ingenting. En sløret skygge på hvert element i en liste er den mest almindelige måde at få en film til at krybe af sted.
Opløsning. Prisen for optegning følger antallet af pixels. 4K kræver fire gange så meget optegning som 1080p på hvert billede, og en vært skalerer alligevel filmen til at passe, så de ekstra pixels ender normalt direkte i skraldespanden. Arbejd i 1920×1080, medmindre nogen faktisk har bedt om mere.
Regnen, der krøb af sted
Herfra stammer bibliotekets canvas-primitiv.
En AI-assistent blev inde i editoren bedt om »Matrix-regn«. Den leverede noget, der tydeligt lignede det rigtige og var helt ubrugeligt. Feltet var bygget på den oplagte måde: én <div> pr. tegn, omkring 1.800 i alt. Hver havde en sløret text-shadow til den grønne glød. Hver havde sit eget data-zxn-element-id, fordi assistenten havde fået at vide, at ting i en film skal have id'er. Og filmen var 3840×2160.
Hver af disse faktorer forstærker de andre. 1.800 elementer er slemt; 1.800 slørede elementer er langt værre; 1.800 slørede elementer ved fire gange så mange pixels giver et billede, der tager sekunder. Editorens tidslinjeskyder blev ubrugelig, og en eksport, der skulle have taget et minut, blev ikke færdig inden for den tid, nogen ville vente.
Det andet problem var mere interessant, fordi det slet ikke handlede om hastighed. Regnen så langsom ud. Strømmene bevægede sig 3 til 8 pixels pr. billede ned gennem et billede på 2.160 pixels, hvilket giver fjorten sekunder til at krydse skærmen, og de gled blødt mellem positionerne. Rigtig digital regn flytter sig en hel tegncelle ad gangen. Glat bevægelse på under én pixel opleves som drift; trinvis bevægelse opleves som hurtig, selv ved samme underliggende hastighed. At gøre filmen hurtigere og få den til at se rigtig ud viste sig at være den samme ændring.
Her er fremgangsmåden i komprimeret form. Kontrolværktøjet afviser den:
import { FullFrame, defineFilm, seededRandom, useTimeline } from "@zxn/motion-core";
type Props = { readonly color: string };
/* Do not do this. One element per glyph, each blurred, at 4K. */
function SlowRain({ color }: Props) {
const { frame } = useTimeline();
const glyphs = Array.from({ length: 1800 }, (_, index) => {
const column = index % 60;
const row = Math.floor(index / 60);
const speed = 3 + seededRandom(`speed-${column}`) * 5;
const y = (row * 72 + frame * speed) % 2160;
return (
<div
key={index}
data-zxn-element-id={`glyph-${index}`}
style={{ position: "absolute", left: column * 64, top: y, color, fontSize: 48, textShadow: `0 0 14px ${color}` }}
>
{index % 2 === 0 ? "0" : "1"}
</div>
);
});
return <FullFrame style={{ background: "#020806" }}>{glyphs}</FullFrame>;
}
export const films = [defineFilm({
id: "slow-rain",
title: "Slow rain",
width: 3840,
height: 2160,
frameRate: 30,
frames: 240,
component: SlowRain,
defaultProps: { color: "#1dff78" },
controls: { color: { type: "color", label: "Rain" } },
})];
Den fungerende udgave er examples/recipes/matrix-rain.tsx: samme billede på ét <Canvas2D> i 1920×1080, der flytter sig én celle ad gangen og kun har glød på hovederne. Den koster omtrent det samme, uanset om der er fyrre kolonner eller fire hundrede. Tegn på canvas gennemgår den.
Et budget
Grove, ærlige tal frem for en formel:
- Fra tiere til få hundrede elementer. Et titelkort er en håndfuld. Et diagram med tyve søjler, deres etiketter og en akse er måske firs. En film om en brugergrænseflade med en scene, et panel og dets indhold kan nå to hundrede. Over et par hundrede bør du kunne forklare hvorfor; ved tusind er der noget galt.
- Sløring på en håndfuld elementer, aldrig i en løkke. Én glød på en titel, én blød skygge under et kort, ét
backdrop-filterpå ét panel. Er en sløring inde i en.map(), så flyt den til et fælles overordnet element, eller flyt hele listen over på et canvas. - Arbejd i 1920×1080, 30 fps. Gå kun højere, når leverancen faktisk kræver det, og forvent at betale fire gange så meget for 4K.
- Ét canvas slår tusind elementer, altid. Et partikelfelt er ét element med én løkke af tegnekald, og tegneløkken er langt billigere end layout og optegning pr. node.
Intet af dette handler om mikrooptimering. Det handler om ikke at bede browseren udføre arbejde, ingen nogensinde vil se.
Element-id'er
data-zxn-element-id markerer noget, en vært kan vælge, navngive og animere — titlen, en søjle, en etiket, en form eller en tekstbjælke. examples/recipes/lower-third.tsx giver id'er til strap, rule, name og role: fire ting, et menneske måske vil tage fat i. <StageItem id> tilføjer et for dig.
Giv ikke partikler id'er. Et id på hvert af 1.800 tegn er 1.800 poster i et egenskabspanel, ingen nogensinde åbner, en attribut, browseren bærer på hver node, og et løfte om, at prik 914 er værd at kunne styre særskilt. Det er den ikke.
Prøven er enkel: Ville et menneske nogensinde klikke på det og forvente, at noget blev valgt? Så får det et id. Ellers bør det sandsynligvis slet ikke være et element.
Determinisme giver ydeevne
Billeder efterspørges i vilkårlig rækkefølge. Afspilningsmarkøren hopper, en afspiller gentager, og en renderer kan søge. Derfor beregner en film sit billede ud fra billednummeret i stedet for at føre en simulering frem trinvis, og den disciplin betaler sig to gange.
Den oplagte gevinst er korrekthed: springValue bruger en lukket formel, og seededRandom er en ren funktion af sin startværdi, så billede 200 tegnet fra bunden er identisk med billede 200 nået ved afspilning fra nul.
Den mindre oplagte gevinst er hastighed. En film, der akkumulerer, kan ikke tegne billede 200 uden at tegne de 199 før det. En film, der udleder, kan tegne ethvert billede med det samme, hvilket gør tidslinjeskyderen responsiv og lader en vært rendere billeder i vilkårlig rækkefølge eller parallelt. useState, setInterval, requestAnimationFrame, Date.now() og CSS transition/animation fejler alle denne prøve — se Tænk i billeder.
Tjekliste
- Alt partikellignende på et
<Canvas2D>, ikke i DOM'en. - Antal elementer i tiere eller få hundrede.
- Sløring på en håndfuld elementer; ingen inde i en løkke.
- 1920×1080, medmindre der er en grund til andet.
- Id'er på ting, et menneske ville vælge, ikke på partikler.
- Alt udledt af billednummeret: ingen tilstand, intet ur, ingen tilfældighed uden startværdi.
- Biblioteker oprettet pauset og med afspilningsposition sat på hvert billede, aldrig afspillet.
Hvad kontrolværktøjet fanger
check_film i MCP-serveren (se AI-agenter) kører typekontrollen og en kort liste af regler for kodevaner på filmens kildekode uden at udføre den, så det meste på denne side håndhæves frem for at skulle huskes. Det markerer:
Array.from({ length: N })med tre eller flere cifre — hundredvis af elementer eller flere, hvilket hører hjemme på et canvas.setIntervalellerrequestAnimationFrame— et ur i stedet for billednummeret.Date.now(),new Date()ellerperformance.now()— aflæsning af det rigtige ur.Math.random()— brugseededRandom(seed), så et billede altid tegner det samme.createNoise3D()og lignende kaldt uden argument — et felt uden startværdi giver en ny film ved hver rendering..play(),.restart()eller.resume()— opret bibliotekets tidslinje pauset, og sæt dens position.- CSS
transition:,animation:eller@keyframes— browserens ur, som ikke passer med eksporten. useState(elleruseReducer(— tilstand, der akkumuleres på tværs af billeder, ødelægger søgning med tidslinjeskyderen.- En
textShadow,boxShadow,filter: blurellerbackdropFilterinde i en.map()eller enArray.from({ … }, callback)— sløring ganget med en liste. - En film større end 1920×1080.
- En prop i
defaultPropsuden kontrol.
Det rapporterer også typefejl mod SDK'ets rigtige deklarationer og afviser importer uden for det tilladte sæt. Advarsler er råd; fejl er ikke. En bevidst 4K-leverance kan udgives med en advarsel ved sit navn, men det bør være bevidst.