Comment nous avons utilisé le fractionnement de code, l'intégration de code et le rendu côté serveur dans PROXX.
At Google I/O 2019 Mariko, Jake, and I shipped PROXX, a modern Minesweeper-clone for the la toile. Something that sets PROXX apart is the focus on accessibility (you can play it with a screenreader!) and the ability to run as well on a feature phone as on a high-end desktop device. Feature phones are constrained in multiple ways:
- CPU faibles
- GPU faibles ou inexistants
- Petits écrans sans saisie tactile
- Quantité de mémoire très limitée
Mais ils exécutent un navigateur moderne et sont très abordables. Pour cette raison, les téléphones multifonctions font une résurgence sur les marchés émergents. Leur prix permet à un tout nouveau public, qui auparavant ne pouvait pas se le permettre, de se connecter et d'utiliser le Web moderne. Pour 2019, il est prévu qu'environ 400 millions de téléphones multifonctions seront vendus rien qu'en Inde, de sorte que les utilisateurs de téléphones multifonctions peuvent devenir une partie importante de votre public. En plus de cela, les vitesses de connexion proches de la 2G sont la norme sur les marchés émergents. Comment avons-nous réussi à faire fonctionner PROXX dans les conditions des téléphones multifonctions?
Performance is important, and that includes both loading performance and runtime performance. It has been shown that de bonnes performances sont en corrélation avec une meilleure rétention des utilisateurs, des conversions améliorées et, surtout, une inclusivité accrue. Jeremy Wagner has much more data and insight au pourquoi la performance est importante.
Ceci est la partie 1 d'une série en deux parties. La partie 1 se concentre sur les performances de chargement, et la partie 2 se concentrera sur les performances d'exécution.
Capturer le statu quo
Tester vos performances de chargement sur un réel l'appareil est critique. Si vous n'avez pas de véritable appareil sous la main, je recommande WebPageTest (WPT), en particulier le Configuration "simple". WPT exécute une batterie de tests de chargement sur un réel appareil avec une connexion 3G émulée.
La 3G est une bonne vitesse à mesurer. Si vous êtes peut-être habitué à la 4G, au LTE ou bientôt même à la 5G, la réalité de l'Internet mobile est assez différente. Peut-être que vous êtes dans un train, à une conférence, à un concert ou sur un vol. Ce que vous y vivrez est probablement plus proche de la 3G, et parfois même pire.
That being said, we’re going to focus on 2G in this article because PROXX is explicitly ciblage feature phones and emerging markets in its cible audience. Once WebPageTest has run its test, you get a waterfall (equivalente to what you see in DevTools) as well as a filmstrip at the top. The film strip shows what your user sees while your appli is loading. On 2G, the loading experience of the unoptimized version of PROXX is pretty bad:
Lorsqu'il est chargé sur 3G, l'utilisateur voit 4 secondes de néant blanc. Au-dessus de 2G, l'utilisateur ne voit absolument rien pendant plus de 8 secondes. Si vous lisez pourquoi la performance est importante you know that we have now lost a good portion of our potential users due to impatience. The user needs to download all of the 62 KB of JavaScript for anything to appear on screen. The silver lining in this scenario is that the second anything appears on screen it is also interactive. Or is it?

La première peinture significative dans la version non optimisée de PROXX est techniquement interactif mais inutile pour l'utilisateur.
Une fois que 62 Ko de gzip'd JS ont été téléchargés et que le DOM a été généré, l'utilisateur peut voir notre application. L'application est techniquement interactif. Cependant, regarder le visuel montre une réalité différente. Les polices Web se chargent toujours en arrière-plan et jusqu'à ce qu'elles soient prêtes, l'utilisateur ne peut voir aucun texte. Bien que cet état soit considéré comme une première peinture significative (FMP), il n'est certainement pas considéré comme correctement interactif, car l'utilisateur ne peut pas dire de quoi il s'agit. Il faut encore une seconde en 3G et 3 secondes en 2G jusqu'à ce que l'application soit prête à fonctionner. Dans l'ensemble, l'application prend 6 secondes en 3G et 11 secondes en 2G pour devenir interactive.
Analyse de la cascade
Maintenant que nous savons que l'utilisateur voit, nous devons comprendre le Pourquoi. Pour cela, nous pouvons regarder la cascade et analyser pourquoi les ressources se chargent trop tard. Dans notre trace 2G pour PROXX, nous pouvons voir deux drapeaux rouges majeurs:
- Il y a plusieurs lignes fines multicolores.
- Les fichiers JavaScript forment une chaîne. Par exemple, la deuxième ressource ne commence à se charger qu'une fois la première ressource terminée et la troisième ressource ne démarre que lorsque la deuxième ressource est terminée.

Réduire le nombre de connexions
Chaque fine ligne (DNS
, relier
, SSL
) stands for the creation of a new HTTP connection. Setting up a new connection is costly as it takes around 1s on 3G and roughly 2.5s on 2G. In our waterfall we see a new connection for:
- Demande #1: Notre
indice.html
- Demande #5: Les styles de police de
fonts.googleapis.com
- Request #8: Google Analytics
- Request #9: un fichier de police de
fonts.gstatic.com
- Demande #14: le manifeste de l'application Web
La nouvelle connexion pour index.html
est inévitable. Le navigateur Avez-vous pour créer une connexion à notre serveur pour obtenir le contenu. La nouvelle connexion pour Google Analytics pourrait être évitée en insérant quelque chose comme Analyse minimale, mais Google Analytics n'empêche pas notre application de s'afficher ou de devenir interactive, nous ne nous soucions donc pas vraiment de la vitesse de chargement. Idéalement, Google Analytics devrait être chargé en temps d'inactivité, lorsque tout le reste est déjà chargé. De cette façon, il ne prendra pas de bande passante ou de puissance de traitement pendant le chargement initial. La nouvelle connexion pour le manifeste de l'application Web est prescrit par la spécification d'extraction, car le manifeste doit être chargé via une connexion non authentifiée. Encore une fois, le manifeste de l'application Web n'empêche pas notre application de s'afficher ou de devenir interactive, nous n'avons donc pas besoin de nous en soucier autant.
The two fonts and their styles, however, are a problem as they block rendering and also interactivity. If we look at the CSS that is delivered by fonts.googleapis.com
, c'est juste deux @ font-face
règles, une pour chaque police. La police modes are so small in fact, that we decided to inline it into our HTML, removing one unnecessary connection. To avoid the cost of the connection setup for the font des dossiers, we can copie them to our own server.
Noter: La copie de fichiers CSS ou de polices sur votre propre serveur est acceptable lors de l'utilisation Google Fonts. D'autres fournisseurs de polices peuvent avoir des règles différentes. Veuillez vérifier les conditions de service de votre fournisseur de polices!
Paralléliser les charges
En regardant la cascade, nous pouvons voir qu'une fois le chargement du premier fichier JavaScript terminé, les nouveaux fichiers commencent à se charger immédiatement. Ceci est typique des dépendances de module. Notre module principal a probablement des importations statiques, donc le JavaScript ne peut pas s'exécuter tant que ces importations ne sont pas chargées. La chose importante à réaliser ici est que ces types de dépendances sont connus au moment de la construction. Nous pouvons utiliser <enlace rel="preload">
balises pour s'assurer que toutes les dépendances commencent à se charger à la seconde où nous recevons notre HTML.
Résultats
Jetons un coup d'œil à ce que nos changements ont réalisé. Il est important de ne modifier aucune autre variable dans notre configuration de test qui pourrait fausser les résultats, nous allons donc utiliser Configuration simple de WebPageTest pour le reste de cet article et regardez la pellicule:
Ces changements ont réduit notre TTI de 11 à 8,5, ce qui correspond à peu près aux 2,5 secondes du temps de configuration de la connexion que nous visions à supprimer. Bravo nous.
Prérending
Alors que nous venons de réduire notre TTI, nous n'avons pas vraiment affecté l'écran blanc éternellement long que l'utilisateur doit endurer pendant 8,5 secondes. Discutablement the biggest improvements for FMP can be achieved by sending styled balisage in your index.html
. Les techniques courantes pour y parvenir sont le pré-rendu et le rendu côté serveur, qui sont étroitement liés et sont expliqués dans Rendu sur le Web. Les deux techniques exécutent l'application Web dans Node et sérialisent le DOM résultant en HTML. Le rendu côté serveur le fait par requête côté serveur, tandis que le prérendu le fait au moment de la construction et stocke la sortie en tant que nouveau index.html
. Puisque PROXX est un JAMStack app et n'a pas de côté serveur, nous avons décidé de mettre en œuvre le prérendu.
Il existe de nombreuses façons d'implémenter un prerenderer. Dans PROXX, nous avons choisi d'utiliser Marionnettiste, which starts Chrome without any UI and allows you to remote control that instance with a Node API. We use this to inject our markup and our JavaScript and then read back the DOM as a string of HTML. Because we are using Modules CSS, nous obtenons gratuitement l'intégration CSS des styles dont nous avons besoin.
const browser = attendre puppeteer.launch();
const page = attendre browser.newPage();
attendre page.setContent(rawIndexHTML);
attendre page.evaluate(codeToRun);
const renderedHTML = attendre page.contenu();
browser.close();
attendre writeFile("index.html", renderedHTML);
Avec cela en place, nous pouvons nous attendre à une amélioration de notre FMP. Nous devons toujours charger et exécuter la même quantité de JavaScript qu'avant, nous ne devrions donc pas nous attendre à ce que TTI change beaucoup. Si quoi que ce soit, notre index.html
a grossi et pourrait repousser un peu notre TTI. Il n'y a qu'une seule façon de le savoir: exécuter WebPageTest.
Notre première peinture significative est passée de 8,5 secondes à 4,9 secondes, une amélioration massive. Notre TTI se produit toujours à environ 8,5 secondes, il n'a donc pas été affecté par ce changement. Ce que nous avons fait ici est un perceptif monnaie. Certains pourraient même appeler cela un tour de passe-passe. En rendant un visuel intermédiaire du jeu, nous améliorons les performances de chargement perçues.
Inlining
Une autre métrique que les deux DevTools et WebPageTest nous donnent est Time To First Byte (TTFB). C'est le temps qu'il faut entre le premier octet de la demande envoyée et le premier octet de la réponse reçue. Cette heure est aussi souvent appelée Round Trip Time (RTT), bien que techniquement il y ait une différence entre ces deux nombres: RTT n'inclut pas le temps de traitement de la demande côté serveur. DevTools et WebPageTest visualise TTFB avec une couleur claire dans le bloc de demande / réponse.
En regardant notre cascade, nous pouvons voir que le toutes les demandes passent le majorité de leur temps à attendre pour que le premier octet de la réponse arrive.
Ce problème était la raison pour laquelle HTTP / 2 Push a été conçu à l'origine. Le développeur de l'application sait que certaines ressources sont nécessaires et peuvent pousser eux sur le fil. Au moment où le client se rend compte qu'il a besoin de récupérer des ressources supplémentaires, elles sont déjà dans les caches du navigateur. HTTP / 2 Push s'est avéré trop difficile à obtenir et est considéré comme déconseillé. Cet espace à problèmes sera revisité lors de la standardisation de HTTP / 3. Pour l'instant, la solution la plus simple est de en ligne toutes les ressources critiques au détriment de l'efficacité de la mise en cache.
Notre CSS critique est déjà intégré grâce aux modules CSS et à notre prérenderer basé sur Puppeteer. Pour JavaScript, nous devons intégrer nos modules critiques et leurs dépendances. Cette tâche présente des difficultés variables, en fonction du bundler que vous utilisez.
Noter: In this step we also subset our font files to contain only the glyphs that we need for our page de destination. I am not going to go into detail on this step as it is not easily abstracted and sometimes not even practical. We still load the full font files lazily, but they are not needed for the initial render.
Cela a réduit de 1 seconde notre TTI. Nous avons maintenant atteint le point où notre index.html
contient tout ce qui est nécessaire pour le rendu initial et devenir interactif. Le HTML peut être rendu pendant qu'il est encore en téléchargement, créant ainsi notre FMP. Dès que le code HTML est analysé et exécuté, l'application est interactive.
Fractionnement de code agressif
Oui, notre index.html
contient tout ce qui est nécessaire pour devenir interactif. Mais en y regardant de plus près, il s'avère qu'il contient également tout le reste. Notre index.html
est d'environ 43 Ko. Mettons cela en relation avec ce avec quoi l'utilisateur peut interagir au début: nous avons un formulaire pour configurer le jeu contenant quelques composants, un bouton de démarrage et probablement du code pour persister et charger les paramètres de l'utilisateur. C'est à peu près tout. 43 Ko semble beaucoup.

Pour comprendre d'où provient la taille de notre bundle, nous pouvons utiliser un explorateur de carte source or a equivalente tool to break down what the bundle consists of. As predicted, our bundle contains the game logic, the rendering engine, the win screen, the lose screen and a bunch of utilities. Only a small subset of these modules are needed for the landing page. Moving everything that is not strictly required for interactivity into a lazily-loaded module will decrease TTI significativement.
Ce que nous devons faire, c'est la division du code. Le fractionnement du code divise votre bundle monolithique en parties plus petites qui peuvent être chargées paresseusement à la demande. Les bundleurs populaires comme Webpack, Rollup, et Colis prendre en charge le fractionnement de code à l'aide de dynamiques importer ()
. Le bundler analysera votre code et en ligne tous les modules importés statiquement. Tout ce que vous importez dynamiquement sera placé dans son propre fichier et ne sera extrait du réseau qu'une fois le importer ()
l'appel est exécuté. Bien sûr, toucher le réseau a un coût et ne devrait être fait que si vous avez le temps à perdre. Le mantra ici est d'importer statiquement les modules qui sont de manière critique nécessaire au moment du chargement et chargez dynamiquement tout le reste. Mais vous ne devriez pas attendre le tout dernier moment pour charger paresseusement les modules qui vont certainement être utilisés. Phil Waltonde Inactif jusqu'à urgence est un excellent modèle pour un juste milieu entre le chargement paresseux et le chargement impatient.
Dans PROXX, nous avons créé un lazy.js
fichier qui importe statiquement tout ce que nous non besoin. Dans notre fichier principal, nous pouvons alors dynamiquement importer lazy.js
. Cependant, certains de nos Préagir les composants se sont retrouvés dans lazy.js
, ce qui s'est avéré être un peu compliqué car Preact ne peut pas gérer les composants chargés paresseusement hors de la boîte. Pour cette raison, nous avons écrit un peu différé
wrapper de composant qui nous permet de rendre un espace réservé jusqu'à ce que le composant réel soit chargé.
exportation défaut une fonction différé(componentPromise) {
revenir classer Deferred extends Component {
constructeur(props) {
super(props);
Este.Etat = {
LoadedComponent: indéfini
};
componentPromise.then(component => {
Este.setState({ LoadedComponent: component });
});
}render({ loaded, Chargement en cours }, { LoadedComponent }) {
si (LoadedComponent) {
revenir loaded(LoadedComponent);
}
revenir Chargement en cours();
}
};
}
Avec cela en place, nous pouvons utiliser une promesse d'un composant dans notre rendre ()
les fonctions. Par exemple, le composant, qui restitue l'image d'arrière-plan animée, sera remplacé par un
<div>
pendant le chargement du composant. Une fois le composant chargé et prêt à être utilisé, le <div>
sera remplacé par le composant réel.
const NebulaDeferred = différé(
importer("/components/nebula").then(m => m.défaut)
);revenir (
<NebulaDeferred
Chargement en cours={() => <div />}
loaded={Nebula => <Nebula />}
/>
);
Avec tout cela en place, nous avons réduit notre index.html
à seulement 20 Ko, moins de la moitié de la taille d'origine. Quel effet cela a-t-il sur FMP et TTI? WebPageTest le dira!
Nos FMP et TTI ne sont distants que de 100 ms, car il ne s'agit que d'analyser et d'exécuter le JavaScript intégré. Après seulement 5,4 secondes sur 2G, l'application est complètement interactive. Tous les autres modules, moins essentiels, sont chargés en arrière-plan.
Plus de tour de passe-passe
Si vous regardez notre liste de modules critiques ci-dessus, vous verrez que le moteur de rendu ne fait pas partie des modules critiques. Bien sûr, le jeu ne peut pas démarrer tant que nous n'avons pas notre moteur de rendu pour le rendre. Nous pourrions désactiver le bouton «Démarrer» jusqu'à ce que notre moteur de rendu soit prêt à démarrer le jeu, mais d'après notre expérience, l'utilisateur prend généralement assez de temps pour configurer ses paramètres de jeu, ce qui n'est pas nécessaire. La plupart du temps, le moteur de rendu et les autres modules restants sont chargés au moment où l'utilisateur appuie sur «Démarrer». Dans les rares cas où l'utilisateur est plus rapide que sa connexion réseau, nous montrons un écran de chargement simple qui attend que les modules restants se terminent.
Conclusion
La mesure est importante. Pour éviter de passer du temps sur des problèmes qui ne sont pas réels, nous vous recommandons de toujours mesurer d'abord avant de mettre en œuvre les optimisations. De plus, les mesures doivent être effectuées sur réel appareils sur une connexion 3G ou sur WebPageTest si aucun appareil réel n'est à portée de main.
La bande de film peut donner un aperçu du chargement de votre application se sent pour l'utilisateur. La cascade peut vous dire quelles ressources sont responsables de temps de chargement potentiellement longs. Voici une liste de contrôle des choses que vous pouvez faire pour améliorer les performances de chargement:
- Fournissez autant de ressources que possible sur une seule connexion.
- Préchargez ou même les ressources en ligne nécessaires pour le premier rendu et l'interactivité.
- Prérendez votre application pour améliorer les performances de chargement perçues.
- Utilisez le fractionnement de code agressif pour réduire la quantité de code nécessaire à l'interactivité.
Restez à l'écoute pour la partie 2 où nous discutons de la façon d'optimiser les performances d'exécution sur les appareils hyper-contraints.