Chargement de script moderne

Passer le bon code pour chaque navigateur n'est pas une tâche facile.

Dans cet article, nous examinerons plusieurs options pour résoudre ce problème.



Passer du code moderne par un navigateur moderne peut améliorer considérablement les performances. Vos packages JavaScript pourront contenir une syntaxe moderne plus compacte ou optimisée et prendre en charge des navigateurs plus anciens.

Parmi les outils pour les développeurs, le modèle de module / nomodule de chargement déclaratif du code moderne ou hérité domine, ce qui fournit aux navigateurs des sources et vous permet de décider lesquels utiliser:

<script type="module" src="/modern.js"></script> <script nomodule src="/legacy.js"></script> 

Malheureusement, tout n'est pas si simple. L'approche HTML illustrée ci-dessus déclenche un rechargement de script dans Edge et Safari .

Que peut-on faire?


Selon le navigateur, nous devons fournir l'une des options pour les scripts compilés, mais quelques anciens navigateurs ne prennent pas en charge toute la syntaxe nécessaire pour cela.

Tout d'abord, il y a Safari Fix . Safari 10.1 prend en charge les modules JS, pas l'attribut nomodule dans les scripts, ce qui lui permet d'exécuter du code moderne et hérité. Cependant, l'événement beforeload non standard pris en charge par Safari 10 et 11 peut être utilisé pour polyfill le nomodule .

Première méthode: téléchargement dynamique


Vous pouvez contourner ces problèmes en implémentant un petit chargeur de script. Similaire au fonctionnement de LoadCSS . Au lieu d'espérer la mise en œuvre de modules ES et de l'attribut nomodule dans les nomodule , vous pouvez essayer d'exécuter un script de module en tant que «test avec un test décisif», et en fonction du résultat, choisissez de télécharger un code moderne ou hérité.

 <!-- use a module script to detect modern browsers: --> <script type="module"> self.modern = true </script> <!-- now use that flag to load modern VS legacy code: --> <script> addEventListener('load', function() { var s = document.createElement('script') if ('noModule' in s) { // notice the casing s.type = 'module' s.src = '/modern.js' } else { s.src = '/legacy.js' } document.head.appendChild(s) }) </script> 

Mais avec cette approche, vous devez attendre la fin du script du module "tournesol" avant d'implémenter le script correct. Cela se produit car <sript type="module"> fonctionne toujours de manière asynchrone. Mais il y a une meilleure façon!

Vous pouvez implémenter l'option indépendante en vérifiant si le nomodule est nomodule en nomodule dans le navigateur. Cela signifie que nous considérerons les navigateurs comme Safari 10.1 comme obsolètes, même s'ils prennent en charge les modules. Mais ça pourrait être pour le mieux . Voici le code pertinent:

 var s = document.createElement('script') if ('noModule' in s) { // notice the casing s.type = 'module' s.src = '/modern.js' } else s.src = '/legacy.js' } document.head.appendChild(s) 

Cela peut être rapidement transformé en une fonction qui charge du code moderne ou hérité, et en fournit également le chargement asynchrone:

 <script> $loadjs("/modern.js","/legacy.js") function $loadjs(src,fallback,s) { s = document.createElement('script') if ('noModule' in s) s.type = 'module', s.src = src else s.async = true, s.src = fallback document.head.appendChild(s) } </script> 

Quel est le compromis ici?

Précharge

Étant donné que la solution est complètement dynamique, le navigateur ne sera pas en mesure de détecter nos ressources JavaScript tant qu'il n'aura pas démarré le code d'amorçage que nous avons écrit pour insérer des scripts modernes ou hérités. En règle générale, un navigateur analyse HTML pour les ressources qu'il peut télécharger à l'avance. Ce problème est résolu, mais pas idéalement: vous pouvez précharger la version moderne du package dans les navigateurs modernes en utilisant <link rl=modulpreload> .

Malheureusement, jusqu'à présent, seul Chrome prend en charge le modulepreload .

 <link rel="modulepreload" href="/modern.js"> <script type="module">self.modern=1</script> <!-- etc --> 

Si cette technique vous convient, vous pouvez réduire la taille du document HTML dans lequel vous intégrez ces scripts. Si votre charge utile est petite, comme un écran de démarrage ou un code de téléchargement d'application client, il est peu probable que la suppression du scanner de précharge affecte les performances. Et si vous dessinez beaucoup de code HTML important sur le serveur à envoyer aux navigateurs, le scanner de précharge vous sera utile et l'approche décrite ne sera pas la meilleure option pour vous.

Voici à quoi pourrait ressembler cette solution en cours d'utilisation:

 <link rel="modulepreload" href="/modern.js"> <script type="module">self.modern=1</script> <script> $loadjs("/modern.js","/legacy.js") function $loadjs(e,d,c){c=document.createElement("script"),self.modern?(c.src=e,c.type="module"):c.src=d,document.head.appendChild(c)} </script> 

Il convient également de noter que la liste des navigateurs qui prennent en charge les modules JS est presque la même que ceux qui prennent en charge <link rl=preload> . Pour certains sites, il peut être approprié d'utiliser <link rl=preload as=script crossorigin> au lieu de modulepreload . Les performances peuvent se détériorer car le préchargement de script classique n'implique pas une analyse uniforme dans le temps, comme c'est le cas avec le modulepreload de modulepreload .

Deuxième méthode: suivre l'agent utilisateur


Je n'ai pas d'exemple de code approprié, car le suivi de l'agent utilisateur est une tâche non triviale. Mais alors vous pouvez lire l'excellent article dans Smashing Magazine.

En fait, tout commence avec le même <scrit src=bundle.js> en HTML pour tous les navigateurs. Lorsque bundle.js est demandé, le serveur analyse la chaîne d'agent utilisateur du navigateur demandeur et sélectionne le JavaScript à renvoyer - moderne ou hérité, selon la façon dont le navigateur a été reconnu.

L'approche est universelle, mais entraîne de graves conséquences:

  • Étant donné que des serveurs intelligents sont requis, cette approche ne fonctionnera pas dans des conditions de déploiement statiques (générateurs de sites statiques, Netlify, etc.).
  • La mise en cache de ces URL JavaScript dépend désormais de l'agent utilisateur, qui est très volatile.
  • La définition de l'AU est difficile et peut conduire à une fausse classification.
  • La ligne User Agent est facile à usurper et de nouveaux UA apparaissent chaque jour.

Un moyen de contourner ces limitations consiste à combiner le modèle de module / nomodule avec la différenciation de l'agent utilisateur pour éviter d'envoyer plusieurs versions du package à la même adresse. Cette approche réduit la mise en cache des pages, mais offre un préchargement efficace: le serveur générateur de HTML sait quand utiliser le modulepreload et quand preload .

 function renderPage(request, response) { let html = `<html><head>...`; const agent = request.headers.userAgent; const isModern = userAgent.isModern(agent); if (isModern) { html += ` <link rel="modulepreload" href="modern.mjs"> <script type="module" src="modern.mjs"></script> `; } else { html += ` <link rel="preload" as="script" href="legacy.js"> <script src="legacy.js"></script> `; } response.end(html); } 

Pour les sites qui génèrent déjà du HTML sur le serveur en réponse à chaque demande, cela peut être une transition efficace vers le téléchargement de scripts modernes.

Méthode 3: beaux vieux navigateurs


L'effet négatif du modèle de module / nomodule est visible dans les anciennes versions de Chrome, Firefox et Safari - leur nombre est très faible, car les navigateurs sont mis à jour automatiquement. Avec Edge 16-18, la situation est différente, mais il y a de l'espoir: les nouvelles versions d'Edge utiliseront le moteur de rendu basé sur Chromium, qui n'a pas de tels problèmes.

Pour certaines applications, ce serait un compromis idéal: téléchargez la version moderne du code dans 90% des navigateurs et donnez du code hérité aux anciens. La charge dans les anciens navigateurs augmentera.

Soit dit en passant, aucun des agents utilisateurs pour lesquels un tel redémarrage est un problème n'occupe une part importante du marché mobile. Il est donc peu probable que la source de tous ces octets supplémentaires soit des appareils mobiles ou des appareils avec un processeur faible.

Si vous créez un site qui est principalement accessible par des navigateurs mobiles ou récents, alors pour la plupart de ces utilisateurs, le type le plus simple de modèle de module / nomodule convient. Assurez-vous simplement d'ajouter le correctif Safari 10.1 si des appareils iOS plus anciens vous parviennent.

    iOS-. <!-- polyfill `nomodule` in Safari 10.1: --> <script type="module"> !function(e,t,n){!("noModule"in(t=e.createElement("script")))&&"onbeforeload"in t&&(n=!1,e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove())}(document) </script> <!-- 90+% of browsers: --> <script src="modern.js" type="module'></script> <!-- IE, Edge <16, Safari <10.1, old desktop: --> <script src="legacy.js" nomodule async defer></script> 

Méthode quatre: appliquer les termes du package


Une bonne solution serait d'utiliser nomodule pour télécharger conditionnellement des packages avec du code qui n'est pas nécessaire dans les navigateurs modernes, tels que les polyfills. Avec cette approche, dans le pire des cas, le polyfill sera chargé ou même exécuté (dans Safari 10.1), mais l'effet de ceci sera limité au «re-polyfilling». Considérant qu'aujourd'hui l'approche avec le téléchargement et l'exécution de polyfills dans tous les navigateurs prévaut, cela peut être une amélioration digne.

 <!-- newer browsers will not load this bundle: --> <script nomodule src="polyfills.js"></script> <!-- all browsers load this one: --> <script src="/bundle.js"></script> 

Vous pouvez configurer la CLI angulaire pour utiliser cette approche avec les polyfills, comme l'a démontré Minko Gachev. Ayant appris cette approche, je me suis rendu compte que vous pouvez activer l'injection automatique de polyfill dans preact-cli - ce PR montre à quel point il est facile de mettre en œuvre cette technique.

Et si vous utilisez WebPack, il existe un plugin pratique pour html-webpack-plugin , qui facilite l'ajout d'un nomodule aux packages avec des polyfills.

Alors que choisir?


La réponse dépend de votre situation. Si vous créez une application cliente et que votre code HTML contient un peu plus que <sript> , vous aurez peut-être besoin de la première méthode .

Si vous créez un site qui est rendu sur le serveur et que vous pouvez vous permettre la mise en cache, la deuxième méthode peut vous convenir.

Si vous utilisez le rendu universel , le gain de performances offert par le scan de pré-chargement peut être très important. Par conséquent, faites attention aux troisième ou quatrième méthodes. Choisissez ce qui convient à votre architecture.

Personnellement, je choisis, en me concentrant sur la durée de l'analyse sur les appareils mobiles, et non sur le coût du téléchargement dans les versions de bureau. Les utilisateurs mobiles perçoivent les coûts d'analyse et de transfert de données comme des dépenses réelles (consommation de batterie et frais de transfert de données), tandis que les utilisateurs de bureau n'ont pas de telles restrictions. Je procède également à l'optimisation pour 90% des utilisateurs - le public principal de mes projets utilise des navigateurs modernes et / ou mobiles.

Que lire


Vous souhaitez en savoir plus sur ce sujet? Vous pouvez commencer ici:

Source: https://habr.com/ru/post/fr473994/


All Articles