AL+

Limba: RO (română) EN (English)

Scrieți-ne: paul@alplustech.com

React Compiler și useMemo: ce optimizează și ce lasă deoparte în codul existent

React Compiler sare peste componentele care încalcă Rules of React, fără să oprească build-ul. Într-o aplicație preluată, folosiți-l întâi ca audit.

Read this article in English

React Compiler nu a făcut performanța în React gratuită. A făcut în schimb ca regulile React (Rules of React) să poată fi verificate automat. Un cod care respecta deja aceste reguli primește memoizare automată aproape fără efort. Un cod plin de mutații și de ocolișuri prin useEffect primește mult mai puțin, pentru că compilatorul lasă deoparte, fără zgomot, componentele problematice. Faptul că aceste componente ar fi și lente este o ipoteză de verificat prin măsurare, nu o garanție a compilatorului. Pentru o echipă care preia o aplicație React existentă, compilatorul e util mai întâi ca audit: arată unde codul nu respectă modelul React. Ștergerea apelurilor useMemo e ultimul pas al acestei munci, nu primul.

Ce s-a lansat și ce s-a promis

Pe 15 mai 2024, la React Conf, echipa React a anunțat că React Compiler devine open source și a publicat o versiune experimentală. Ideea era simplă: un instrument care rulează la build și memoizează automat componentele și hook-urile, adică reține rezultatele între randări, ca dezvoltatorii să nu mai scrie de mână useMemo, useCallback și React.memo.

Condițiile au venit odată cu anunțul, în discuțiile grupului de lucru al compilatorului. Cu două săptămâni înainte de conferință, echipa a explicat că compilatorul validează static regulile React și că, atunci când găsește o încălcare, sare peste componenta sau hook-ul respectiv și compilează restul codului (30 aprilie 2024). Cu două zile înainte, notele despre compatibilitatea cu bibliotecile au adăugat cealaltă jumătate: acolo unde nu poate stabili dacă un cod e valid, de exemplu când acesta apelează o funcție externă, compilatorul presupune că regulile sunt respectate (13 mai 2024).

Așadar, din prima zi compilatorul a luat două tipuri de decizii fără să-i spună mare lucru celui care răspunde de aplicație: sare peste ce vede că e greșit și are încredere în ce nu poate vedea. Primele reacții nu au citit lucrurile așa. Unul dintre primele comentarii din discuția de pe Hacker News din ziua lansării îl descria ca pe o baghetă magică ce avea să accelereze foarte multe site-uri și aplicații existente fără schimbări majore în cod.

Proiectul a avansat apoi repede. Versiunea beta a apărut pe 21 octombrie 2024, cu suport pentru React 17 și 18 printr-un pachet separat, iar versiunea 1.0 pe 7 octombrie 2025, când regulile de linting ale compilatorului au trecut în eslint-plugin-react-hooks.

Compilatorul sare peste cod fără să vă anunțe

Comportamentul implicit contează mai mult decât anunțul. Opțiunea panicThreshold are valoarea implicită 'none': componentele care nu pot fi compilate sunt lăsate deoparte, build-ul continuă, iar acele componente rulează normal, fără optimizările React Compiler; procesarea obișnuită la build continuă. Documentația recomandă această setare pentru orice build de producție.

Pentru un instrument care nu are voie să strice build-ul, e alegerea corectă. Înseamnă însă că, dacă activați compilatorul într-un cod vechi, obțineți un build reușit și o aplicație care merge, fără nicio indicație despre cât din ea a fost optimizat. Funcțiile ocolite pot avea încălcări ale regulilor sau alte motive de excludere. Citiți diagnosticul înainte să decideți unde e nevoie de o corectură.

Un caz tipic este o componentă care își modifică direct datele primite:

function OrderList({ orders }) {
  for (const order of orders) {
    order.label = `${order.number} (${order.customer})`;
  }
  return orders.map((order) => <OrderRow key={order.id} order={order} />);
}

Codul modifică obiecte care aparțin componentei-părinte. Suita de teste a compilatorului conține exact acest tipar ca exemplu de eroare, cu mesajul „Modifying component props or hook arguments is not allowed. Consider using a local variable instead.” Corectura e mică și ține întâi de corectitudine, abia apoi de performanță:

function OrderList({ orders }) {
  const rows = orders.map((order) => ({
    ...order,
    label: `${order.number} (${order.customer})`,
  }));
  return rows.map((row) => <OrderRow key={row.id} order={row} />);
}

După ce componenta nu-și mai modifică datele primite, compilatorul o poate memoiza. Până atunci, o lasă așa cum a fost scrisă și nu vă spune nimic, dacă nu verificați explicit.

Cazul mai grav: codul în care compilatorul are încredere

Componentele ocolite sunt o ocazie pierdută. Problema mai scumpă e codul pe care compilatorul îl compilează pentru că nu a putut detecta încălcarea. Ghidul de depanare spune că majoritatea problemelor întâlnite apar la rulare și sunt chiar de acest fel, iar cauza frecventă e codul care se bazează pe memoizare ca să funcționeze corect. Efectele care depind de faptul că un obiect își păstrează referința, listele de dependențe care au nevoie de valori stabile și cache-urile bazate pe compararea referințelor pot porni prea des, pot intra în buclă sau pot rata actualizări atunci când memoizarea se schimbă.

De aceea, anunțul versiunii 1.0 recomandă ca în codul existent useMemo și useCallback să rămână pe loc, pentru că eliminarea lor poate schimba rezultatul compilării, sau să fie eliminate doar după teste atente. Recomand și fixarea compilatorului la o versiune exactă dacă acoperirea cu teste e slabă, pentru că o versiune viitoare poate memoiza altfel și poate schimba momentul în care pornesc acele efecte. Într-o aplicație preluată, acoperirea slabă cu teste e de obicei punctul de plecare.

Cel mai puternic argument împotrivă

Chiar recomandările echipei React merg în sens invers. Pentru adoptare, grupul de lucru propune ordinea „make it work, make it right, make it faster”: activați compilatorul, verificați că aplicația merge, măsurați și abia apoi corectați încălcările raportate de linter. Textul spune clar că nu e deloc necesar să corectați întâi încălcările ca să vedeți beneficii.

Cifrele Meta susțin această ordine. Anunțul versiunii beta spune că Meta a trecut pe compilator mai multe aplicații web mari, printre care Facebook și Threads, cu puține modificări de cod, într-un monorepo cu peste 100.000 de componente. Tot acolo se arată că doar aproximativ 8% dintre pull requesturile React ale companiei foloseau memoizare manuală și că scrierea lor dura cu 31-46% mai mult. Anunțul versiunii 1.0 raportează îmbunătățiri de până la 12% la încărcarea inițială și la navigarea între pagini în Meta Quest Store.

Cred că recomandarea e corectă pentru tipul de cod pe care îl descrie. Aplicațiile Meta fuseseră, în cuvintele echipei, optimizate de mână ani la rând de inginerii Meta și de experți React, într-o companie care a scris chiar regulile. O aplicație care a trecut prin mai multe echipe, niciuna dintre ele cu linterul pornit, e cu totul altceva. Merită și acolo să activați compilatorul, dar un build reușit vă spune foarte puțin, iar numai măsurarea poate arăta dacă părțile lente coincid cu cele ocolite. Codul valid poate fi lent, iar o încălcare a regulilor poate avea un cost mic la rulare.

Ordinea de lucru într-o aplicație preluată

Pentru o echipă care tocmai a preluat o aplicație React, aceasta este ordinea pe care o susțin:

  1. Rulați linterul înaintea compilatorului. Regulile compilatorului pot fi folosite fără compilator încă din versiunea beta, iar acum vin în eslint-plugin-react-hooks. Raportul arată unde aplicația nu respectă modelul React, iar fiecare încălcare e un posibil bug, cu sau fără compilator.
  2. Faceți lista componentelor pe care compilatorul le-ar ocoli. Opțiunea logger distinge între CompileError, pentru funcții ocolite din cauza erorilor, și CompileSkip, pentru alte motive de excludere. Înregistrați-le pe amândouă, alături de CompileSuccess, și citiți motivul; nu orice excludere este o încălcare a regulilor. Puneți lista lângă o înregistrare din profiler a ecranelor lente.
  3. Corectați încălcările acolo unde cele două liste se suprapun, cu câte un test pentru fiecare comportament atins. Sunt corecturi de corectitudine și merită făcute chiar dacă nu veți activa niciodată compilatorul.
  4. Activați compilatorul în etape. Ghidul de adoptare treptată descrie activarea pe directoare, activarea explicită cu "use memo" și activarea la rulare printr-un feature flag. Folosiți "use no memo" doar ca excludere temporară, cu un comentariu care spune de ce.
  5. Eliminați memoizarea manuală la final, componentă cu componentă, și doar acolo unde testele acoperă efectele care depind de ea.

Auditul aduce valoare chiar dacă, în final, câștigul de performanță se dovedește mic. O listă a componentelor care își modifică datele primite sau citesc ref-uri în timpul randării e o listă a locurilor în care aplicația e fragilă deja azi.

Unde concluzia nu se aplică

O aplicație nouă ar trebui să pornească direct cu compilatorul activat. De la versiunea 1.0, Expo SDK 54 îl activează implicit, iar Vite și Next.js oferă șabloane de proiect cu compilatorul inclus. Un cod scris cu linterul pornit din prima zi are prea puțin de auditat.

Compilatorul nu e soluția nici atunci când lentoarea nu vine din randări repetate. Documentația spune că memoizarea lui vizează în primul rând performanța la actualizare și că memoizează doar componente și hook-uri, fără să împartă rezultatele între ele. Un API lent, un bundle prea mare sau o funcție costisitoare apelată din multe componente cer altă rezolvare.

După doi ani și jumătate

În 2024, discuția s-a purtat mai ales în jurul întrebării dacă useMemo mai poate fi șters. Astăzi, întrebarea mai utilă e ce a refuzat compilatorul să compileze și de ce. Compilatorul nu a luat responsabilitatea pentru performanță de pe umerii echipei care deține codul. Diagnosticele ajută la găsirea încălcărilor regulilor și a altor limite de compilare. Echipa trebuie în continuare să le interpreteze, să măsoare rezultatul și să răspundă de corecturi.

Surse

Toate sursele sunt în limba engleză.