Optimització de JavaScript

Ramon Saquete

Escrit per Ramón Saquete

Amb el pas dels usuaris de dispositius d’escriptori a dispositius mòbils, l’optimització de les pàgines web en el client cobra major interès que en el passat, equiparant en importància a les optimitzacions al servidor. I és que aconseguir que un web funcioni ràpid en uns dispositius que són més lents i amb connexions que, depenent de la cobertura, poden ser dolentes o fins i tot patir talls, no és una tasca fàcil, però és necessari, ja que disposar de major velocitat implicarà usuaris més satisfets que ens visitaran més sovint i millor posicionament en recerques des de mòbil.

en aquest article vaig a tractar alguns dels punts en els quals cal prestar més atenció a l’hora de l’optimització de l’ JavaScript que s’executa en el client. En primer lloc veurem com optimitzar la seva descàrrega i compilació, i en segon lloc, com optimitzar la seva execució perquè la pàgina tingui un bon rendiment. Entenent per rendiment, la definició que ens dóna el model RAIL de Google. Aquestes sigles signifiquen: a

  • Resposta de la interfície en menys de 100ms.
  • Animacions amb dibuixat complet cada 16ms que són 60 FPS o 60 imatges per segon.
  • Inhabilitat: quan l’usuari no interactua amb la pàgina el que s’executa en segon pla no ha de durar més de 50ms.
  • Load: la pàgina de carregar en 1000ms.

Aquests temps hem aconseguir-los en el pitjor dels casos (quan s’executa el web en un mòbil antic), amb pocs recursos de processador i memòria.

Temps de càrrega de web en mòbil comparada amb escriptori.
Comparació de la càrrega d’un lloc a mòbil i en escriptori obtingudes de la base de dades pública Chrome User Experience Report. L’histograma mostra el nombre d’usuaris que van obtenir cada temps de resposta. Com es pot observar el pic màxim en mòbil està en 2.5sy en escriptori en 1.5s.

A continuació explico, en primer lloc, les accions necessàries per optimitzar la descàrrega i compilació i, en segon lloc, les accions necessàries per a optimitzar l’execució de el codi, sent aquesta part més tècnica, però no menys important.

accions per optimitzar la descàrrega i compilació de el codi JavaScript

escorcollar al navegador

Aquí tenim dues opcions. La primera és fer servir l’API Cache de JavaScript, de la qual podem fer ús instal·lant un service worker. La segona és utilitzar la memòria cau de l’protocol HTTP. Si fem servir l’API Cache nostra aplicació podria tenir l’opció de funcionar en mode desconnectat. Si fem servir la memòria cau de l’protocol HTTP, a grans trets, hem de configurar fent servir el paràmetre Cache-control amb els valors public i max-age, amb un temps de memòria cau gran, com per exemple un any. Després, si volem invalidar aquesta memòria cau canviarem el nom a l’arxiu.

Comprimir amb Brotli Q11 i Gzip

A l’comprimir el codi JavaScript, estem reduint els bits que es transmeten per la xarxa i , per tant, el temps de transmissió, però cal tenir en compte que estem augmentant el temps de processament tant al servidor com en el client, ja que el primer haurà de comprimir l’arxiu i el segon descomprimir-lo. Podem estalviar el primer temps si tenim una memòria cau d’arxius comprimits al servidor, però el temps de descompressió en el client més el temps de transmissió comprimit, pot ser més gran que el temps de transmissió de l’arxiu descomprimit, fent que aquesta tècnica faci la descàrrega més lenta. Això passarà només amb arxius molt petits i amb velocitats de transmissió altes. La velocitat de transmissió de l’usuari no la podem saber, però sí que se li pot dir al nostre servidor que no comprimeixi els arxius molt petits, per exemple, dir-li que no comprimeixi els arxius menors a 280 bytes. En connexions amb velocitats altes, per sobre dels 100Mb / s, aquest valor hauria de ser molt més gran, però es tracta d’optimitzar per a aquells que tenen connexions mòbils amb poca cobertura, on la pèrdua de rendiment és més acusada, encara que en connexions ràpides vagi una mica més lent.

El nou algoritme de compressió Brotli millora la compressió respecte a Gzip un 17%. Sí el navegador envia, a la capçalera de l’protocol HTTP, el valor “br” dins el paràmetre accept-encoding, això significarà que el servidor pot enviar l’arxiu en format Brotli en lloc d’en Gzip.

Minimitzar

Consisteix en utilitzar una eina automàtica per eliminar comentaris, espais, tabuladors i substituir variables per fer que el codi ocupi menys espai.Els arxius minimitzats han cachearse al servidor o generar-ja minimitzats a l’hora de pujar-los, ja que si el servidor ha de minimitzar-amb cada petició, repercutirà negativament en el rendiment.

Unificar el codi JavaScript

Aquesta és una tècnica d’optimització que no té molta importància si la nostra web funciona amb HTTPS i HTTP2, ja que aquest protocol envia els arxius com si fossin un de sol, però si la nostra web treballa amb HTTP1.1 o esperem tenir molts clients amb navegadors antics que usin aquest protocol, la unificació és necessària per optimitzar la descàrrega. Però no cal passar-se i unificar tot el codi del web en únic arxiu, ja que si s’envia només el codi que necessita l’usuari en cada pàgina, es poden reduir bastant els bytes a descarregar. Per a això separarem el codi base que és necessari per tot el portal del que es va a executar a cada pàgina individual. D’aquesta manera tindrem dos arxius estigui habilitat per a cada pàgina, un amb les llibreries bàsiques que serà comú per a totes les pàgines i un altre amb el codi específic de la pàgina. Amb una eina com webpack podem unificar i minimitzar aquests dos grups d’arxius en el nostre projecte de desenvolupament. Procura que l’eina que facis servir per això et generi els anomenats “source maps”. Aquests són arxius .map que s’associen a la capçalera de l’arxiu final i en els quals s’estableix la relació entre el codi minimitzat i unificat i els arxius reials de codi font. D’aquesta manera després podem depurar el codi sense problemes.

L’opció d’unificar tot en un únic arxiu més gran, té el costat bo que es pot escorcollar tot el codi JavaScript del web al navegador, a la primera visita i, en la següent càrrega, l’usuari no haurà de descarregar tot el codi JavaScript. Així que aquesta opció la recomano només si l’estalvi de bytes és pràcticament menyspreable respecte a la tècnica anterior i tenim una taxa de rebot baixa.

Marca el JavaScript com asíncron

Hem d’incloure el JavaScript de la següent manera:

<script async src="/codigo.js" />

d’aquesta manera estem evitant que l’aparició de l’etiqueta script bloquegi l’etapa de construcció de DOM de la pàgina.

No fer servir JavaScript encastat a la pàgina

a l’usar l’etiqueta script per encastar codi a la pàgina, també es bloqueja la construcció de DOM i més encara si es fa servir la funció document.write ( ). En altres paraules, això està prohibit:

< script > documente.write ( “Hello world!”) ; < / script >

Carregar el JavaScript a la capçalera de la pàgina amb async

Abans de disposar de l’etiqueta async, es recomanava posar totes les etiquetes de script a la fi de la pàgina per evitar bloquejar la construcció d’aquesta. Això ja no cal, de fet és millor que estigui a dalt dins de l’etiqueta < head >, perquè el JavaScript es comenci a descarregar, analitzar i compilar el més aviat possible, ja que aquestes fases són les que més van a trigar. Si no es fa servir aquest atribut, el JavaScript ha d’estar a al final.

Eliminar el JavaScript que no s’utilitza

En aquest punt no només estem reduint el temps de transmissió, sinó també el temps que el porta a el navegador analitzar i compilar el codi. Per fer-ho hem de tenir en compte els següents punts:

  • Si es detecta que una funcionalitat no està sent utilitzada pels usuaris, podem eliminar-la amb tot el seu codi JavaScript associat, de manera que el web carregarà més ràpid i els usuaris ho agrairan.
  • També és possible que hàgim inclòs per error alguna llibreria que no sigui necessària o que tinguem llibreries que ofereixin alguna funcionalitat de la que ja disposem de forma nativa en tots els navegadors, sense necessitat d’usar codi addicional i de manera més ràpida.
  • Finalment, si volem optimitzar a l’extrem, sense importar el temps que porti, hauríem d’eliminar dins de les llibreries el codi de què no estem fent ús. Però no ho recomano, perquè mai sabem quan ens pot tornar a fer falta.

Ajornar la càrrega de l’JavaScript que no sigui necessari:

S’ha de fer amb aquelles funcionalitats que no són necessàries per al dibuixat inicial de la pàgina. Són funcionalitats per a les quals l’usuari haurà de realitzar una determinada acció per executar-la. D’aquesta manera evitem carregar i compilar codi JavaScript que retardaria la visualització inicial. Un cop carregada completament la pàgina, podem començar la càrrega d’aquestes funcionalitats perquè sigui immediata quan l’usuari comenci a interactuar.Google en el model RAIL recomana que aquesta càrrega ajornada es faci en blocs de 50ms perquè no influeixi amb la interacció de l’usuari amb la pàgina. Si l’usuari interactua amb una funcionalitat que encara no ha estat carregada, haurem carregar-la en aquest moment.

Accions per optimitzar l’execució de el codi JavaScript

Evitar utilitzar massa memòria

no es pot dir quanta memòria serà massa, però sí que es pot dir que sempre hem de tractar no utilitzar més del necessari, perquè no sabem quanta memòria tindrà el dispositiu que executarà el web. Quan s’executa el recol·lector d’escombraries de el navegador, s’atura l’execució de JavaScript, i això passa cada vegada que el nostre codi sol·licita a el navegador reservar nova memòria. Si això passa amb freqüència la pàgina funcionarà amb lentitud.

A la pestanya “Memory” de les eines per a desenvolupadors de Chrome, podem veure la memòria ocupada per cada funció de JavaScript:

Memòria reservada per funció de JavaScript.
Memòria reservada per funció

Evitar fuites de memòria

Si tenim una fuita de memòria en un bucle, la pàgina anirà reservant cada vegada més memòria ocupant tota la disponible de el dispositiu i fent que tot vagi cada vegada més lent. Aquesta fallada es sol donar en carrusels i sliders d’imatges.

En Chrome podem analitzar si la nostra web té fuites de memòria amb l’enregistrament d’una línia de temps a la pestanya rendiment de les eines per a desenvolupadors:

Visualització de fugida de memòria a la pestanya de performance de Google Chrome.
Aquest és l’aspecte que té una fugida de memòria a la pestanya “Performance” de Google Chrome on podem observar un creixement constant de nodes de DOM i de l’Heap de JS.

Normalment les fuites de memòria vénen per trossos de DOM que s’eliminen de la pàgina però que tenen alguna variable que els fa referència i, per tant, el recol·lector d’escombraries no pot eliminar-los i per no entendre com funciona l’àmbit de les variables i les clausures en JavaScript .

Arbres desacoblats de DOM.
Arbres desacoblats de DOM (detached DOM tree) que estan ocupant memòria perquè hi ha variables que els fan referència.

Utilitza web workers quan necessitis executar codi que necessiti molt temps d’execució

Tots els processadors avui en dia són multifil i multinucli però JavaScript tradicionalment ha estat un llenguatge monofil i, encara que té temporitzadors, aquests s’executen en el mateix fil d’execució, en el qual, a més, s’executa la interacció amb la interfície, així que mentre s’executa JavaScript la interfície es bloqueja i si triga més de 50ms serà perceptible. Els web workers i service workers ens porten l’execució multifil a JavaScript, encara que no permeten l’accés directe a DOM, així que haurem de pensar com retardar l’accés a aquest, per poder aplicar web workers en els casos que tinguem codi que tard més de 50ms a executar-se.

Utilitza l’API Fetch (AJAX)

l’ús de l’API Fetch o AJAX també és una bona manera que l’usuari percebi un temps de càrrega més ràpid, però no hem de fer-lo servir en la càrrega inicial, sinó en la navegació subsegüent i de manera que sigui indexable. Per a això la millor manera d’implementar-és fer ús d’un framework que usi Universal JavaScript.

Prioritza l’accés a variables locals

estigui habilitat primer busca si la variable existeix de forma local i segueix buscant-la en els àmbits superiors, sent les últimes les variables globals. JavaScript accedeix més ràpid a variables locals perquè no ha de buscar la variable en àmbits superiors per trobar-la, així que és una bona estratègia guardar en variables locals aquelles variables d’un àmbit superior a les que anem a accedir diverses vegades i, a més, no crear nous àmbits amb clausures o amb les sentències with i try catch, sense que sigui necessari.

Si accedeixes diverses vegades a un element de DOM guarda en una variable local

l’accés a DOM és lent. Així que si anem a llegir el contingut d’un element diverses vegades, millor guarda’l en una variable local, així el JavaScript no haurà de buscar l’element al DOM cada vegada que vulguis accedir al seu contingut. Però posa atenció, si guardes en una variable un tros de DOM que després vas treure de la pàgina i no faràs servir més, procura després assignar a “null” la variable on t’ho havies guardat per no provocar una fuita de memòria.

Agrupar i minimitzar les lectures i escriptures de DOM i CSSOM

Quan el navegador dibuixa una pàgina recorre la ruta de representació crítica que segueix els següents passos en la primera càrrega:

  1. es rep l’HTML.
  2. Comença a construir-se el DOM.
  3. Mentre es construeix el DOM, se sol·liciten els recursos externs (CSS i JS ).
  4. es construeix el CCSOM (barreja de DOM i el CSS).
  5. es crea l’arbre de representació (són les parts de l’CSSOM que es van a dibuixar).
  6. a partir d’l’arbre de representació es calcula la geometria de cada part visible de l’arbre en una capa. Aquesta etapa es diu layout o reflow.
  7. En l’etapa final de pintat, es pinten les capes de el pas 6 que es processen i van component una damunt d’una altra per mostrar la pàgina a l’usuari.
  8. Si s’ha acabat de compilar el JavaScript, aquest s’executa (en realitat, aquest pas es podria ocórrer en qualsevol punt després de la passa 3, sent millor com més aviat).
  9. Si en el pas anterior el codi JavaScript obliga a refer part de DOM o el CSSOM tornem diversos passos enrere que s’executaran fins al punt 7.

Construcció de l'arbre de representació o arbre de renderitzat.
Construcció de l’arbre de representació

Tot i que els navegadors van encolant els canvis de l’arbre de representació i decideixen quan fer el repintat, si tenim un bucle en el qual llegim el DOM i / o el CSSOM i el modifiquem en la següent línia, pot ser que el navegador es vegi f orsat a executar el reflow o repintar la pàgina diverses vegades, sobretot si la lectura següent depèn de l’escriptura anterior. Per això és recomanable:

  • Separar totes les lectures en un bucle independent i fer totes les escriptures d’una sola vegada amb la propietat cssText si és el CSSOM o innerHTML si és el DOM, així el navegador només haurà de llançar un repintat.
  • Si les lectures depenen de les escriptures anteriors, busca una manera de reescriure l’algorisme perquè això no sigui així.
  • Si no tens més remei que aplicar molts canvis a un element de DOM, treu-lo de DOM, fes els canvis i torna-ho a introduir on era.
  • A Google Chrome, podem analitzar el que passa a la ruta de representació crítica amb l’eina Lighthouse de la pestanya “Audits” oa la pestanya “Performance” gravant el que passa mentre es carrega la pàgina.
anàlisi de rendiment de l'eina Lighthouse.
En aquesta anàlisi de rendiment d’Lighthouse de la pàgina principal de Google, podem veure quins recursos bloquegen la ruta de representació crítica.

Fes servir la funció requestAnimationFrame (callback) en animacions i els efectes que depenen de l’scroll

la funció requestAnimationFrame (), fa que la funció que se li passa com a paràmetre no provoqui un repintat, fins al següent programat. Això, a més d’evitar repintats innecessaris, té l’efecte de que les animacions s’aturen mentre l’usuari està en una altra pestanya, estalviant CPU i bateria de el dispositiu.

Els efectes que depenen de l’scroll són els més lents perquè les següents propietats de DOM forcen 1 reflow (pas 7 del punt anterior) a l’accedir-hi:

offsetTop, offsetLeft, offsetWidth, offsetHeightscrollTop, scrollLeft, scrollWidth, scrollHeightclientTop, clientLeft, clientWidth, clientHeightgetComputedStyle() (currentStyle en IE)

Si a més d’accedir a una d’aquestes propietats, després en base a elles vam pintar un banner o menú que et segueixen a l’moure l’scroll o un efecte de scroll parallax, es farà el repintat de diverses capes cada vegada que desplacem el scroll, afectant negativament el temps de resposta de la interfície, de manera que podem tenir un scroll que vagi donant salts en lloc de lliscar suaument. Per això, amb aquests efectes, s’ha de guardar en una variable global l’última posició de l’scroll en l’esdeveniment onscroll, i desprès utilitzar la funció requestAnimationFrame () només si l’anterior animació ha acabat.

Si hi ha molts esdeveniments semblants agrupa’ls

Si tens 300 botons que a l’clicar fan pràcticament el mateix, podem assignar un esdeveniment a l’element pare dels 300 botons en lloc d’assignar 300 esdeveniments a cada un d’ells. A l’fer clic en un botó, l’esdeveniment “bombolleja” fins al pare i des d’aquest podem saber a quina botó va fer clic l’usuari i modificar el comportament en conseqüència.

Compte amb els esdeveniments que es disparen diverses vegades seguides

Els esdeveniments com onmousemove o onscroll, es disparen diverses vegades seguides mentre es realitza l’accció. Així que procura controlar que el codi associat no s’executi més vegades de les necessàries, ja que aquest és un error bastant comú.

Evita l’execució de cadenes amb codi amb eval (), Function (), setTimeout () i setInterval ()

El fet d’introduir codi en un literal perquè sigui analitzat i compilat durant l’execució de la resta de el codi és força lent, per exemple: eval ( “c = a + b”) ;. Sempre es pot refer la programació per evitar haver de fer això.

Implementa les optimitzacions que aplicaries en qualsevol altre llenguatge de programació

  • Fes servir sempre els algoritmes amb la menor complexitat computacional o complexitat ciclomática per la tasca a resoldre.
  • Fes servir les estructures de dades òptimes per aconseguir el punt anterior.
  • Reescriu l’algoritme per obtenir el mateix resultat amb menys càlculs.
  • Evita crides recursives, canviant l’algoritme per un equivalent que faci ús d’una pila.
  • Fes que una funció amb un cost alt i repetides trucades al llarg de diferents blocs de codi guardi en memòria el resultat per a la propera trucada.
  • Posa en variables els càlculs i crides a funcions repetides.
  • a l’hora de recórrer un bucle, guarda’t primer la mida de l’bucle en una variable, per evitar calcular de nou, en la seva condició de finalització, en cada iteració.
  • Factoritza i simplifica les fórmules matemàtiques.
  • Reemplaça càlculs que no depenguin de variables per constants i deixa el càlcul comentat.
  • Fes servir arrays de cerca: serveixen per obtenir un valor en base a un altre en lloc d’utilitzar un bloc switch.
  • Fes que les condicions sempre siguin amb major probabilitat certes per aprofitar millor l’execució especulativa de l’processador, ja que així la predicció de salts fallarà menys.
  • Simplifica expressions booleanes amb les regles de la lògica booleana o millor encara amb mapes de Karnaugh.
  • Fes servir els operadors a nivell de bit quan puguis usar-los per substituir certes operacions, ja que aquests operadors fan servir menys cicles de processador . Usar-los requereix saber aritmètica binària, per exemple: sent x un valor d’una variable sencera, podem posar “i = x > > 1; ” en lloc de “i = x / 2;” o “i = x & 0xFF;” en lloc de “i = x% 256”.

Aquestes són algunes de les meves preferides, sent les més importants les tres primeres i les que més estudi i pràctica requereixen. Les últimes són micro-optimitzacions que només mereixen la pena si les portes a terme mentre escrius el codi o si és una cosa computacionalment molt costós com un editor de vídeo o un videojoc, tot i que en aquests casos serà millor que facis servir WebAssembly en lloc de JavaScript.

Eines per detectar problemes

Ja hem vist diverses. De totes elles, Lighthouse és la més fàcil d’interpretar, ja que simplement ens dóna una sèrie de punts a millorar, com també ens pot donar l’eina Google PageSpeed Insights o, moltes altres, com GTmetrix. A Chrome també podem usar, en l’opció “Més eines” de menú principal, l’administrador de tasques, per veure la memòria i la CPU utilitzada per cada pestanya. Per a anàlisi encara més tècnics, tenim les eines per a desenvolupadors de Firefox i Google Chrome, on disposem de la pestanya anomenada “Rendiment” i que ens permet analitzar prou bé els temps de cada fase, les fuites de memòria, etc. Vegem un exemple:

Analísis de rendiment de Google Chrome.
En l’anàlisi de rendiment de Google Chrome , al menú d’eines ens permet simular una CPU i una xarxa més lentes, i en ell veiem, entre altres coses, les imatges o quadres per segon (recordem que ha de ser menor a 16ms) i les fases de la ruta de representació crítica amb colors: en blau el temps de càrrega dels arxius, en groc el temps d’execució dels scripts, en morat el temps de construcció de l’arbre de representació (incloent el reflows o construcció de l’layout) i en verd el temps de pintat. A més, apareix el temps que ha portat pintar cada frame i com ha quedat aquest.

Tota la informació que veiem a dalt es pot gravar mentre es carrega la pàgina , executem una acció o fem scroll. Després podem fer zoom a una part de l’gràfic per veure-la en detall i, si com en aquest cas, el que més triga és l’execució de JavaScript, podem desplegar l’apartat Main i punxar sobre dels scripts que porten més temps. Així l’eina ens mostrarà detalladament en la pestanya Bottom-Up com està afectant el JavaScript a cada fase de la ruta de representació crítica i en la pestanya Summary ens indicarà amb un avís si ha detectat un problema de rendiment en el JavaScript. Punxant sobre de l’arxiu que indica, ens portarà concretament a la línia que produeix el retard.

Resum de l'eina DevTools amb avís de problema de rendiment.
Avís de la pestanya resum de les eines de rendiment

Finalment, per a anàlisi encara més fins, és recomanable fer servir l’API de JavaScript Navigation Timing que ens permet mesurar detalladament el que triga cada part de nostre codi des de la pròpia programació.

Recomanacions finals

Com veus, l’optimització de JavaScript no és una tasca fàcil i porta un procés d’anàlisi i optimització laboriós que pot sobrepassar fàcilment el pressupost destinat a el desenvolupament que teníem pensat inicialment. Per això no són poques les webs, plugins i temes més famosos per als gestors de contingut habituals que presenten molts dels problemes que he enumerat.

Si la teva web presenta aquests problemes, intenta solucionar primer aquells que major impacte tinguin en el rendiment i procura sempre que les optimitzacions no afectin la mantenibilitat i qualitat de el codi. Per això no recomano l’ús de tècniques d’optimització més extremes, com treure crides a funcions reemplaçant-les per el codi a què diuen, el desenrotllat de bucles, o la utilització de la mateixa variable per a tot, perquè així carregui des cau o des dels registres de l’processador, ja que són tècniques que embruten l’codi i, en el compilat en temps d’execució de JavaScript, ja s’apliquen algunes d’elles. Així que recorda:

El rendiment no és un requisit que hagi d’estar mai per sobre de la facilitat de detectar errors i afegir funcionalitats.

Fes que et vegin

Contacta’ns si vols que t’ajudem a millorar la teva visibilitat online. Explica’ns de què va el teu projecte i et presentarem una proposta personalitzada per a les necessitats del teu negoci.

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *