Site icon Blog ARC Optimizer

du téléchargement de scripts à l'exécution (première partie)


Cet article vous aidera à comprendre les éléments internes de JavaScript – même les parties les plus étranges. Chaque ligne de code que vous écrivez en JavaScript sera parfaitement logique une fois que vous saurez comment il a été interprété par le moteur sous-jacent. Vous apprendrez plusieurs manières de télécharger des scripts en fonction du cas d'utilisation et de la manière dont l'analyseur génère un arbre de syntaxe abstraite et ses méthodes heuristiques lors de l'analyse du code. Intéressons-nous de plus près aux composants internes des moteurs JavaScript – à commencer par le téléchargement de scripts.

Le langage JavaScript est l’un des langages les plus populaires. Fini l'époque où les gens utilisaient JavaScript uniquement pour gérer des écouteurs d'événements DOM et quelques tâches peu exigeantes. Aujourd'hui, vous pouvez créer une application complète à partir de la base en utilisant JavaScript. JavaScript a pris le dessus sur les vents, les terres et les mers. Avec Node.js envahissant la gamme des technologies côté serveur et l'avènement de bibliothèques riches et puissantes et de frameworks tels que React, Angular et Vue, JavaScript a conquis le Web. Les applications envoient beaucoup de JavaScript sur les câbles. Presque toutes les tâches compliquées d'une application sont maintenant implémentées à l'aide de JavaScript.

Bien que tout cela soit formidable, il est décourageant de constater que la plupart de ces applications sont dépourvues d'expérience utilisateur minimale. Nous continuons à ajouter des fonctionnalités à notre application sans prendre en compte ses implications en termes de performances. Il est important de suivre les techniques appropriées pour obtenir un code optimisé.

Dans cette série de didacticiels, nous allons tout d'abord comprendre ce qui ne va pas avec les techniques classiques, puis nous approfondirons l'analyse pour en apprendre davantage sur certaines Aidez-nous à écrire du code optimisé. Nous comprendrons également comment notre code est analysé, interprété et compilé par le moteur JavaScript sous-jacent et ce qui fonctionne le mieux pour nos moteurs. Bien que la syntaxe de JavaScript soit assez facile à comprendre, la compréhension de ses éléments internes est une tâche plus ardue. Nous allons commencer par les bases et finalement reprendre la bête.

Comprendre la balise de script

Considérons un fichier HTML simple:


 < html > 
     < head > 
         < script   src  =  ' ./ js / first.js '  >  </  script > [19659011] < script   src  =  ' ./ js / second.js '  >  </  script [19659008]> 
         < script   src  =  ' ./ js / third.js '  >  </  script > 
         < script   src  =  ' ./ js / third.js '  > >  ] </  script > 
     </  head > 
     < body > 
         < div > [19659000] ] Description de la balise script  </  div > 
     </  corps > 
 </  html > 
 

first.js inclut le code suivant:

 console .  log  ( "fichier first.js" ) 

second.js contient le code suivant :

 console .  log  ( "fichier second.js" ) 

J'ai mis en place un serveur express pour démontrer les concepts expliqués dans l'article. Si vous souhaitez expérimenter en cours de route, n'hésitez pas à cloner mon référentiel GitHub .

Voyons ce qui se passe lorsque nous ouvrons ce fichier HTML dans le navigateur:

< html > < head > < script asynchrone src = ' ./ js / first.js '

> </ script > < script async src = ' ./ js / seconde.js ' > </ script > < script src = ' ./ js / third .js ' > </ script > < script src = ' . /js/fourth.js'[19459004hootingde19659008]>[19659016unset</[19459004Scripts19269004_reverscript/19199099499179499179499179body> < div > [19659045] Description de la balise de script </ de > </ de corps > </ html >

Report de l’exécution des scripts

Lorsque vous ajoutez le mot clé defer dans la balise de script, le navigateur n’exécute ce script qu’après la fin de l’analyse HTML. Différer signifie simplement que l'exécution du fichier est différée ou différée. Le script est téléchargé dans un autre thread et est exécuté uniquement à la fin de l'analyse HTML.


 < html > 
     < head > 
         < script   defer   src  =  ' ./ js / first.js '  >  </  script > 
         < script   différer   src  =  ' ./ js / second.js '  >  </  script > 
         < script   src  =  ' ./ js / third.js '  

> [19659016] </ script > < script src = ' ./ js / quatrième.js ' 19659008]> </ script > </ head > < corps > < div [19659008]> Description de la balise de script </ div [19659008]> </ corps > </ html >

AST Explorer pour vérifier l'AST généré pour mon code. L'AST pour le code ci-dessus ressemblerait à ceci:

  {
   "type" :   "Programme" 
   "start" : [19659199] 0 
   "fin" :   9 
   "corps" :   [
     {
       "type" [19659008]:   "VariableDeclaration" 
       "start" :   0 
       "end" :   9 [1965999] 19659209] "déclarations" :   [
         {
           "type" :   "VariableDéclarator" 
           "start" :   4 
           "fin" :   9 
           "id" :   {
             "type" :   "Identifiant" 
             "début" :   4 
             "fin" :   5 
             "nom" [nom] [[19659008]:   "a" 
          } 
           "init" :   {
             "type" : [19659195] "Littéral" 
             "début" :   8 
             "fin" :   9 
             "valeur ":   2 
            " raw ":  " 2 "
          } 
        } 
      ] 
      " kind ":  " var "
    } 
  ] 
  " sourceType ":  " module "
} 

Essayons de créer sens de l'AST ci-dessus. C’est un objet JavaScript dont les propriétés sont de type début fin body et sourceType . start est l'index du premier caractère et end est la longueur de votre code, qui est var a = 2 dans ce cas. body contient la définition du code. C'est un tableau avec un seul objet puisqu'il n'y a qu'une seule instruction du type VariableDeclaration dans notre programme. À l'intérieur VariableDeclaration il spécifie l'identificateur a et sa valeur initiale est 2 . Vérifiez les objets id et init . Le type de déclaration est var . Cela peut également être let ou const .

Voyons un autre exemple pour mieux comprendre les AST:

  function   foo   ( )   {
     let  bar  =   2 
     retour  bar
} 

Et son AST est comme suit –

  {
   "type" :   "Programme" 
   "start"  :   0 
   "end" :   50 
  "body" :   [
     {
      type ":  " FonctionDéclaration "
      " début ":   0 
      " fin ":   50  , 
       "id" :   {
         "type" :   "identificateur" 
         "start" :   9 [19659008]
         "fin" :   12 
         "nom" :   "foo" 
      } 
       "expression" ":   false 
      " générateur ":   false 
      " params ":   []. 19659008]
       "body" :   {
         "type" :   "BlockStatement" 
         "start" [début]. 19659008]:   16 
         "end" :   50 
         "corps" :   [
           [
             "type" :   "VariableDeclaration" 
             "début" :   22 
             "fin" :   33 [19659008]
             "déclarations" :   [
	 {
                 "type" :   "VariableDeclarator" 
                 "début" . :   26 
                 "end" :   33 
                 "id" :   {
                   "type"  :   "Identificateur" 
                   "début" :   26 
                   "fin" :   29 [196594000] "nom" :   "bar" 
                } 
                 "init" :   {
                   "type" :   "Literal "
                  " début ":   32 
                  " fin ":   33 
                  " valeur "[19659008]:   2 
                   "raw" :   "2" 
                 "
	] :  " [genre19659008]:   "let" 
          } 
           {
             "type" :   "ReturnStatement" 
             "start"  :   38 
             "end" :   48 
             "argument" :   {
	 "type"  :   "Identificateur" 
	 "début" :   45 
	 "fin" :   48 
	 "nom" :   "bar" 
            } 
          } 
        ] 
      } 
    } 
  ] 
   "sourceType"  :   "module" 
} 

Encore une fois, il a des propriétés – de type début fin corps ] et sourceType . start vaut 0, ce qui signifie que le premier caractère est à la position 0, et end vaut 50, ce qui signifie que la longueur du code est 50. body est un tableau avec un objet du type FunctionDeclaration . Le nom de la fonction foo est spécifié dans l'objet id . Cette fonction ne prend aucun argument, donc params est un tableau vide. Le corps de la FunctionDeclaration est de type BlockStatement . BlockStatement identifie la portée de la fonction. Le corps du BlockStatement a deux objets pour VariableDeclaration et ReturnStatement . VariableDeclaration est identique à l'exemple précédent. ReturnStatement contient un argument portant le nom bar car bar est renvoyé par la fonction foo .

C'est ce qu'il est. C'est comment les AST sont générés. Quand j'ai entendu parler d'AST pour la première fois, j'ai pensé à eux comme de gros arbres effrayants aux nœuds compliqués. Mais maintenant que nous connaissons bien les AST, ne pensez-vous pas qu'il ne s'agit que d'un groupe de nœuds bien conçus représentant la sémantique d'un programme?

Parser prend également en charge Scopes.

 
 
 let  globalVar  =   2 
 function   foo   ()   {
     let  globalVar  (19659007) =   3 
    console .  log  ( 'globalVar'  globalVar ) 
} 

La fonction foo est destinée à l'impression 3 et non 2 car la valeur de globalVar dans son étendue est de 3. Lors de l'analyse syntaxique du code JavaScript, l'analyseur génère également les étendues correspondantes.

Lorsqu'un globalVar est référencé dans une fonction foo nous commençons par rechercher globalVar dans le périmètre fonctionnel. Si cette variable n'est pas trouvée dans l'étendue fonctionnelle, nous recherchons son parent, qui est dans ce cas l'objet global . Prenons un autre exemple:

  let  globalVar  =   2 
 function   foo   ()   {
     let  localVar  =   3 
    console .  log  ( 'localVar'  localVar ) 
    console .  log  ( 'globalVar'  globalVar ) 
} 
console .  log  ( 'localVar'  localVar ) 
console .  log  ( 'globalVar'  globalVar ) 

Les instructions de la console dans la fonction foo seraient imprimées 3 et 2 alors que les instructions de la console en dehors de la fonction foo afficheraient undefined et 3. En effet, localVar n'est pas accessible en dehors de la fonction foo . Il est défini dans le domaine d'application de la fonction foo et une recherche de localVar à l'extérieur de ce résultat donne undefined .

L'analyse syntaxique dans V8

V8 utilise deux analyseurs syntaxiques pour analyser le code JavaScript, appelés analyseur et pré-analyseur. Pour comprendre la nécessité de deux analyseurs, considérons le code ci-dessous:

  function   foo   ()   {
    console .  log  ( 'Je suis dans la fonction foo' ) 
} 

 function   bar   () [) 19659207] {
    console .  log  ( "Je suis dans la barre de fonctions" ) 
} 

 / * Fonction d’appel foo * / 
 foo  () 

Lorsque le code ci-dessus est analysé, l'analyseur génère un AST représentant la fonction foo et la fonction bar . Cependant, la fonction bar n'est appelée nulle part dans le programme. Nous passons du temps à analyser et à compiler des fonctions qui ne sont pas utilisées, du moins lors du démarrage. bar peut être appelé ultérieurement, par exemple en cliquant sur un bouton. Mais ce n'est clairement pas nécessaire lors du démarrage. Peut-on gagner ce temps en ne compilant pas la fonction bar lors du démarrage? Oui, nous le pouvons!

L’analyseur est ce que nous faisons jusqu’à présent. Il analyse tout votre code, construit des AST, étend des portées et trouve toutes les erreurs de syntaxe. Le pré-analyseur est comme un analyseur rapide. Il ne compile que ce qui est nécessaire et ignore les fonctions qui ne sont pas appelées. Il construit des portées mais ne construit pas d’AST. Il ne trouve qu'un ensemble limité d'erreurs et est environ deux fois plus rapide que l'analyseur. La V8 utilise une approche heuristique pour déterminer la technique d'analyse au moment de l'exécution.

Prenons un exemple pour comprendre comment la V8 analyse le code JavaScript:

  ( function   foo   ( )   {
    console .  log  ( "Je suis une fonction IIFE" ) 

     fonction   bar   ()   {
        console .  log  ( "Je suis une fonction interne de IIFE" ) 
    } 

 )  ( ) 

Lorsque l’analyseur trouve la parenthèse initiale, il comprend qu’il s’agit d’un IIFE et s’appelle immédiatement, il analyse donc la fonction foo en utilisant un analyseur complet ou un analyseur syntaxique enthousiaste. À l’intérieur foo lorsqu’il rencontre la fonction bar il analyse ou prépare paresseusement la fonction bar car, en se basant sur ses heuristiques, il sait que le La fonction bar ne sera pas appelée immédiatement. Lorsque la fonction foo est complètement analysée, V8 construit son AST ainsi que des oscilloscopes sans créer d’AST pour la fonction bar . Il ne construit que des portées pour la fonction bar .

Avez-vous déjà rencontré ce problème en écrivant le code JavaScript:

function toBeCalled () [} toBeCalled ()

La fonction de BeCalled est analysée paresseusement par le moteur V8, qui utilise alors un analyseur complet pour le faire fonctionner. Le temps passé à analyser paresseusement la fonction toBeCalled est en réalité une perte de temps. La V8 analyse paresseusement la fonction à BeCalled elle ne sait pas que la déclaration immédiate serait un appel à cette fonction. Pour éviter cela, vous pouvez indiquer à V8 quelles fonctions doivent être analysées (analyse complète).

  ( fonction   àBeCalled   ()   {[19659008]} ) 
 àBeCalled  () 

Le fait d'envelopper une fonction entre parenthèses indique à V8 que cette fonction doit être analysée avec précaution. Vous pouvez également ajouter un point d'exclamation avant la déclaration de fonction pour indiquer à V8 d'analyser avec avidité cette fonction.

 !  function   de BeCalled   ()   {[19659008]} 
 àBeCalled  () 

Analyse des fonctions internes

 function   outer   ()   {
     function   inner [19659207] ()   {} 
} 

Dans ce cas, le V8 analyse paresseusement les deux fonctions, external et intérieure . Lorsque nous appelons outer la fonction outer est analysée avec minutie / minutieusement et la fonction inside est à nouveau analysée paresseusement. Cela signifie que la fonction intérieure est analysée deux fois paresseusement. La situation est encore pire lorsque les fonctions sont fortement imbriquées.

  function   outer   ()   {
     function   inner   () 19659207] {
         function   insideInner   ()   {} 
    } 
     retour  interne
} 

Initialement, les trois fonctions extérieures intérieures et à l'intérieur d'Inner sont paresseusement analysées.

  let  innerFn  =   outer  () 
 innerFn  () 

Lorsque nous appelons la fonction outer elle est entièrement analysée et Les fonctions intérieure et à l'intérieur intérieure sont analysées paresseusement. Maintenant, lorsque nous appelons intérieure intérieure est entièrement analysée et intérieure: Inner est analysée paresseusement. Cela fait que insideInner soit analysé trois fois. N'utilisez pas de fonctions imbriquées quand elles ne sont pas nécessaires. Utilisez correctement les fonctions imbriquées!

Analyse syntaxique des fermetures

 ( function   Extérieur   ()   {
     Laisser  de [19459354] a  =   2 
     let  b  =   3 
     fonction   intérieure   ()   {
         retour  a
    } 
     retour  intérieur
} ) 

Dans l'extrait de code ci-dessus, la fonction outer étant entourée de parenthèses, elle est analysée avec impatience. La fonction intérieure est analysée paresseusement. inner renvoie la variable a, qui entre dans le cadre de sa fonction outer . Il s'agit d'un cas valable de fermeture.

  let  innerFn  =   outer  () 
 innerFn  ()  innerFn  renvoie très bien une valeur de 2 puisqu'il a accès à la variable a de sa portée parente. Lors de l'analyse de la fonction  intérieure lorsque V8 rencontre la variable a, il recherche la variable a dans le contexte de la fonction  intérieure . Puisque a n'est pas présent dans le domaine de  inner il le vérifie dans le domaine de la fonction  outer . V8 comprend que la variable a doit être enregistrée dans le contexte de la fonction et doit être conservée même après l'exécution de la fonction  outer . Ainsi, la variable a est stockée dans le contexte de fonction de  outer  et est conservée jusqu'à l'exécution complète de sa fonction dépendante  inner . Veuillez noter que la variable b n'est pas conservée dans ce cas car elle n'est utilisée dans aucune des fonctions internes. 

Lorsque nous appelons la fonction innerFn la valeur de a n'est pas trouvée dans la pile d'appels, nous recherchons ensuite sa valeur dans le contexte de la fonction. Les recherches dans le contexte d'une fonction sont coûteuses comparées aux recherches dans la pile d'appels.

Vérifions le code analysé généré par V8.

  function   fnCalled   ()   {1965} 
    console.log('Inside fnCalled')
}

function fnNotCalled () {
    console.log('Inside fnNotCalled')
}

fnCalled()

As per our understanding, both of these functions will be lazily parsed and when we make a function call to fnCalledit would be fully parsed and print Inside fnCalled. Let’s see this in action. Run the file containing the above code as node --trace_parse parse.js. If you’ve cloned my GitHub repositoryyou’ll find this file under public/js folder. parse.js is the name of the file, and --trace_parse serves as an indicator to the runtime of nodejs to print the parsed output. This command would generate a dump of parsing logs. I’ll save the output of this command in a file parsedOutput.txt. For now, all that makes sense is the below screenshot of the dump.

dump.

Script Streaming

Now that we know how parsing works in V8, let’s understand one concept related to Script Streaming. Script Streaming is effective from Chrome version 41.

From what we’ve learned till now, we know it’s the main thread that parses the JavaScript code (even with async and defer keywords). With Script Streaming in place, now the parsing can happen in another thread. While the script is still getting downloaded by the main thread, the parser thread can start parsing the script. This means that the parsing would be completed in line with the download. This technique proves very helpful for large scripts and slow network connections. Check out the below image to understand how the browser operates with Script Streaming and without Script Streaming.

Kendo UI for jQuery - our complete UI component library that allows you to quickly build high-quality, responsive apps. It includes all the components you’ll need, from grids and charts to schedulers and dials.


Comments are disabled in preview mode.




Source link
Quitter la version mobile