Mejora del rendimiento del paquete JavaScript con división de código

 

 

 

  • SmashingConf Nueva York 2024

  • Índice
    1. Errores de agrupación
    2. Costos de desempeño
    3. División de código
      1. Importaciones dinámicas y división de código en React
      2. Performance Budgets
    4. React Suspense And Server-Side Rendering (SSR)
    5. Benefits And Caveats Of Code-Splitting
      1. Referencias

    En este artículo, Adrian Bece comparte más sobre los beneficios y advertencias de la división de código y cómo se pueden mejorar el rendimiento de la página y los tiempos de carga cargando dinámicamente paquetes de JavaScript costosos y no críticos.

     

    Los proyectos creados utilizando marcos basados ​​en JavaScript a menudo incluyen grandes paquetes de JavaScript que tardan en descargarse, analizarse y ejecutarse, bloqueando la representación de la página y la entrada del usuario en el proceso. Este problema es más evidente en redes lentas y poco confiables y en dispositivos de gama baja. En este artículo, cubriremos las mejores prácticas de división de código y mostraremos algunos ejemplos usando React, de modo que cargamos el JavaScript mínimo necesario para representar una página y cargamos dinámicamente paquetes importantes no críticos.

    Los marcos basados ​​en JavaScript como React hicieron que el proceso de desarrollo de aplicaciones web fuera ágil y eficiente, para bien o para mal. Esta automatización a menudo lleva a los desarrolladores a tratar un marco y crear herramientas como una caja negra. Es un error común pensar que el código producido por las herramientas de construcción del marco (Webpack, por ejemplo) está completamente optimizado y no se puede mejorar más.

    Aunque los paquetes de JavaScript finales están minimizados y sacudidos en árbol, generalmente toda la aplicación web está contenida en uno o solo unos pocos archivos JavaScript , dependiendo de la configuración del proyecto y las características del marco listas para usar. ¿Qué problema podría haber si el archivo en sí está minimizado y optimizado?

    Errores de agrupación

    Echemos un vistazo a un ejemplo sencillo. El paquete JavaScript para nuestra aplicación web consta de las siguientes seis páginas contenidas en componentes individuales. Por lo general, esos componentes constan de aún más subcomponentes y otras importaciones, pero mantendremos esto simple para mayor claridad.

     

    • Cuatro páginas públicas
      Se puede acceder a ellas incluso cuando no se ha iniciado sesión (página de inicio, inicio de sesión, registro y página de perfil).
    • Una única página privada
      a la que se puede acceder iniciando sesión (página del panel).
    • Una página restringida
      Es una página de administración que tiene una descripción general de toda la actividad del usuario, cuentas y análisis (página de administración).

    ( Vista previa grande )

    Cuando un usuario llega a una página de inicio, por ejemplo, se carga y analiza todo el app.min.jspaquete con el código de otras páginas, lo que significa que solo una parte se utiliza y se representa en la página. Esto suena ineficiente , ¿no? Además de eso, todos los usuarios cargan una parte restringida de la aplicación a la que solo unos pocos usuarios podrán tener acceso: la página de administración. Aunque el código está parcialmente ofuscado como parte del proceso de minificación, corremos el riesgo de exponer los puntos finales de la API u otros datos reservados para los usuarios administradores.

    ¿Cómo podemos asegurarnos de que el usuario cargue el mínimo JavaScript necesario para representar la página en la que se encuentra actualmente? Además de eso, también debemos asegurarnos de que los paquetes para secciones restringidas de la página sean cargados únicamente por usuarios autorizados. La respuesta está en la división del código .

    Antes de profundizar en los detalles sobre la división de código, recordemos rápidamente qué hace que JavaScript tenga tanto impacto en el rendimiento general.

    Costos de desempeño

    El efecto de JavaScript en el rendimiento consiste en los costos de descarga, análisis y ejecución .

    Como cualquier archivo al que se hace referencia y se utiliza en un sitio web, primero debe descargarse de un servidor. La rapidez con la que se descarga el archivo depende de la velocidad de la conexión y del tamaño del archivo . Los usuarios pueden navegar por Internet utilizando redes lentas y poco confiables, por lo que la minificación, optimización y división de código de los archivos JavaScript garantizan que el usuario descargue el archivo más pequeño posible.

    Tiempos de carga estimados para una aplicación JavaScript improvisada. Observe la diferencia en los tiempos de carga entre las redes móviles y de cable. Los usuarios tienen diferentes experiencias de carga según el tipo de red. ( Vista previa grande )

    A diferencia del archivo de imagen, por ejemplo, que solo debe representarse una vez que se ha descargado el archivo, los archivos JavaScript deben analizarse , compilarse y ejecutarse . Esta es una operación que requiere un uso intensivo de la CPU y que bloquea el hilo principal, lo que hace que la página no responda durante ese tiempo. Un usuario no puede interactuar con la página durante esa fase aunque el contenido se muestre y aparentemente haya terminado de cargarse. Si el script tarda demasiado en analizarse y ejecutarse, el usuario tendrá la impresión de que el sitio no funciona y lo abandonará. Es por eso que Lighthouse y Core Web Vitals especifican métricas de retardo de la primera entrada (FID) y tiempo total de bloqueo (TBT) para medir la interactividad del sitio y la capacidad de respuesta de las entradas.

     

    JavaScript también es un recurso de bloqueo de procesamiento , lo que significa que si el navegador encuentra un script dentro del documento HTML que no está aplazado, no procesa la página hasta que carga y ejecuta el script. Los atributos HTML asynce deferindican al navegador que no bloquee el procesamiento de la página; sin embargo, el subproceso de la CPU aún se bloquea y el script debe ejecutarse antes de que la página responda a la entrada del usuario.

    El rendimiento del sitio web no es consistente en todos los dispositivos. Existe una amplia gama de dispositivos disponibles en el mercado con diferentes especificaciones de CPU y memoria, por lo que no sorprende que la diferencia en el tiempo de ejecución de JavaScript entre los dispositivos de gama alta y los dispositivos promedio sea enorme.

    Los tiempos de procesamiento de JavaScript son muy diferentes entre los dispositivos de gama alta, media y baja (Fuente de la imagen: ' El coste de JavaScript en 2019 ' de Addy Osmani) ( Vista previa grande )

    Para atender a una amplia gama de especificaciones de dispositivos y tipos de redes, solo debemos enviar código crítico . Para las aplicaciones web basadas en JavaScript, significa que solo se debe cargar el código que se utiliza en esa página en particular, ya que cargar el paquete completo de la aplicación a la vez puede resultar en tiempos de ejecución más largos y, para los usuarios, un tiempo de espera más largo hasta que la página se convierta en utilizable y sensible a la entrada.

    División de código

    Con la división de código, nuestro objetivo es diferir la carga, el análisis y la ejecución de código JavaScript que no es necesario para la página o el estado actual. Para nuestro ejemplo, eso significaría que las páginas individuales deberían dividirse en sus respectivos paquetes: homepage.min.js, login.min.js, dashboard.min.jsetc.

    Cuando el usuario llega inicialmente a la página de inicio, el paquete del proveedor principal que contiene el marco y otras dependencias compartidas debe cargarse junto con el paquete de la página de inicio. El usuario hace clic en un botón que alterna el modo de creación de cuenta. A medida que el usuario interactúa con las entradas, la costosa biblioteca de verificación de seguridad de contraseñas se carga dinámicamente. Cuando un usuario crea una cuenta e inicia sesión correctamente, se le redirige al panel y solo entonces se carga el paquete del panel. También es importante tener en cuenta que este usuario en particular no tiene una función de administrador en la aplicación web, por lo que el paquete de administrador no se carga.

     

    ( Vista previa grande )

    Importaciones dinámicas y división de código en React

    La división de código está disponible de forma inmediata para la aplicación Create React y otros marcos que usan Webpack como Gatsby y Next.js. Si ha configurado el proyecto React manualmente o si está utilizando un marco que no tiene la división de código configurada de fábrica, deberá consultar la documentación de Webpack o la documentación de la herramienta de compilación que estás usando.

    Funciones

    Antes de sumergirnos en los componentes de React de división de código, también debemos mencionar que también podemos codificar funciones de división en React importándolas dinámicamente . La importación dinámica es JavaScript básico, por lo que este enfoque debería funcionar para todos los marcos. Sin embargo, tenga en cuenta que esta sintaxis no es compatible con navegadores antiguos como Internet Explorer y Opera Mini.

    import("path/to/myFunction.js").then((myFunction) = { /* ... */});

    En el siguiente ejemplo, tenemos una publicación de blog con una sección de comentarios. Nos gustaría animar a nuestros lectores a crear una cuenta y dejar comentarios, por lo que ofrecemos una forma rápida de crear una cuenta y comenzar a comentar mostrando el formulario junto a la sección de comentarios si no han iniciado sesión.

    ( Vista previa grande )

    El formulario utiliza una zxcvbnbiblioteca considerable de 800 kB para verificar la seguridad de la contraseña, lo que podría resultar problemático para el rendimiento, por lo que es el candidato adecuado para la división de código. Este es el escenario exacto con el que estuve lidiando el año pasado y logramos lograr un aumento notable en el rendimiento al dividir el código de esta biblioteca en un paquete separado y cargarlo dinámicamente.

    Tamaño del paquete y tiempos de descarga estimados para zxcvbnel paquete. Esta estimación no incluye los tiempos de análisis y ejecución, lo que también afecta el rendimiento del sitio web. ( Vista previa grande )

    Veamos cómo Comments.jsxse ve el componente.

    import React, { useState } from "react";import zxcvbn from "zxcvbn"; /* We're importing the lib directly */export const Comments = () = { const [password, setPassword] = useState(""); const [passwordStrength, setPasswordStrength] = useState(0); const onPasswordChange = (event) = { const { value } = event.target; const { score } = zxcvbn(value) setPassword(value); setPasswordStrength(score); }; return ( form {/* ... */} input onChange={onPasswordChange} type="password"/input smallPassword strength: {passwordStrength}/small {/* ... */} /form );};

    Estamos importando la zxcvbnbiblioteca directamente y, como resultado, se incluye en el paquete principal. ¡El paquete minimizado resultante para nuestro pequeño componente de publicación de blog tiene la friolera de 442kB comprimidos con gzip! La biblioteca de React y esta página de publicación de blog apenas alcanzan los 45 kB comprimidos con gzip, por lo que hemos ralentizado considerablemente la carga inicial de esta página al cargar instantáneamente esta biblioteca de verificación de contraseñas.

     

    ( Vista previa grande )

    Podemos llegar a la misma conclusión observando el resultado de Webpack Bundle Analyzer para la aplicación. Ese rectángulo estrecho en el extremo derecho es el componente de publicación de nuestro blog.

    La aplicación completa está contenida en un único paquete y zxcvbnla biblioteca es la parte más grande del paquete. Es incluso mayor que la dependencia de reaccionar-dom. ( Vista previa grande )

    La verificación de contraseñas no es crítica para la representación de la página. Su funcionalidad es necesaria sólo cuando el usuario interactúa con la entrada de la contraseña. Entonces, dividamos el código zxcvbnen un paquete separado, lo importemos dinámicamente y lo carguemos solo cuando el valor de entrada de la contraseña cambie, es decir, cuando el usuario comience a escribir su contraseña. Necesitamos eliminar la importdeclaración y agregar la declaración de importación dinámica a la onChangefunción del controlador de eventos de contraseña.

    import React, { useState } from "react";export const Comments = () = { /* ... */ const onPasswordChange = (event) = { const { value } = event.target; setPassword(value); /* Dynamic import - rename default import to lib name for clarity */ import("zxcvbn").then(({default: zxcvbn}) = { const { score } = zxcvbn(value); setPasswordStrength(score); }); }; /* ... */}

    Veamos cómo se comporta nuestra aplicación ahora después de haber movido la biblioteca a una importación dinámica.

    .

    Esto se ve mucho mejor. El paquete más pequeño de color azul a la derecha es el paquete "crítico" que se carga instantáneamente, mientras que el paquete grande a la izquierda se carga dinámicamente. ( Vista previa grande )

    Componentes de React de terceros

    Los componentes de React de división de código son simples en la mayoría de los casos y constan de los siguientes cuatro pasos:

    1. usar una exportación predeterminada para un componente que queremos dividir en código;
    2. importar el componente con React.lazy;
    3. renderizar el componente como hijo de React.Suspense;
    4. proporcionar un componente alternativo para React.Suspense.

    Echemos un vistazo a otro ejemplo. Esta vez estamos creando un componente de selección de fechas que tiene requisitos que la entrada de fecha HTML predeterminada no puede cumplir. Hemos elegido react-calendarcomo biblioteca la que vamos a utilizar. Recetas faciles y rápidas

    ( Vista previa grande )

    Echemos un vistazo al DatePickercomponente. Podemos ver que el Calendarcomponente del react-calendarpaquete se muestra condicionalmente cuando el usuario se centra en el elemento de entrada de fecha.

    import React, { useState } from "react";import Calendar from "react-calendar";export const DatePicker = () = { const [showModal, setShowModal] = useState(false); const handleDateChange = (date) = { setShowModal(false); }; const handleFocus = () = setShowModal(true); return ( div label htmlFor="dob"Date of birth/label input onFocus={handleFocus} type="date" onChange={handleDateChange} / {showModal Calendar value={startDate} onChange={handleDateChange} /} /div );};

    Esta es prácticamente una forma estándar en la que casi cualquiera habría creado esta aplicación. Ejecutemos Webpack Bundle Analyzer y veamos cómo se ven los paquetes.

     

    ( Vista previa grande )

    Al igual que en el ejemplo anterior, toda la aplicación se carga en un único paquete de JavaScript y react-calendarocupa una parte considerable del mismo. Veamos si podemos dividirlo en código.

    Lo primero que debemos tener en cuenta es que la Calendarventana emergente se carga condicionalmente, sólo cuando showModalse establece el estado. Esto convierte al Calendarcomponente en un candidato ideal para la división de código.

    A continuación, debemos verificar si Calendares una exportación predeterminada. En nuestro caso, lo es.

    import Calendar from "react-calendar"; /* Standard import */

    Cambiemos el DatePickercomponente para cargarlo de forma diferida Calendar.

    import React, { useState, lazy, Suspense } from "react";const Calendar = lazy(() = import("react-calendar")); /* Dynamic import */export const DateOfBirth = () = { const [showModal, setShowModal] = useState(false); const handleDateChange = (date) = { setShowModal(false); }; const handleFocus = () = setShowModal(true); return ( div input onFocus={handleFocus} type="date" onChange={handleDateChange} / {showModal ( Suspense fallback={null} Calendar value={startDate} onChange={handleDateChange} / /Suspense )} /div );};

    Primero, debemos eliminar la importdeclaración y reemplazarla con lazyuna declaración de importación. A continuación, debemos envolver el componente con carga diferida en un Suspensecomponente y proporcionar un fallbackque se renderice hasta que el componente con carga diferida esté disponible.

    Es importante tener en cuenta que fallbackes un accesorio obligatorio del Suspensecomponente. Podemos proporcionar cualquier nodo React válido como alternativa:

    • null
      Si no queremos que se renderice nada durante el proceso de carga.
    • string
      Si queremos simplemente mostrar un texto.
    • Elementos de carga de esqueleto del componente React , por ejemplo.

    Ejecutemos Webpack Bundle Analyzer y confirmemos que el react-calendarcódigo se ha dividido correctamente del paquete principal.

    ( Vista previa grande )

    Componentes del proyecto

    No estamos limitados a componentes de terceros o paquetes NPM. Podemos dividir el código de prácticamente cualquier componente de nuestro proyecto. Tomemos las rutas del sitio web, por ejemplo, y dividamos con código los componentes de las páginas individuales en paquetes separados. De esa manera, siempre cargaremos solo el paquete principal (compartido) y un paquete de componentes necesarios para la página en la que nos encontramos actualmente.

     

    Nuestro principal App.jsxconsta de un enrutador React y tres componentes que se cargan según la ubicación actual (URL).

    import { Navigation } from "./Navigation";import { Routes, Route } from "react-router-dom";import React from "react";import Dashboard from "./pages/Dashboard";import Home from "./pages/Home";import About from "./pages/About";function App() { return ( Routes Route path="/" element={Home /} / Route path="/dashboard" element={Dashboard /} / Route path="/about" element={About /} / /Routes );}export default App;

    Cada uno de esos componentes de página tiene una exportación predeterminada y actualmente se importa de forma predeterminada y no diferida para este ejemplo.

    import React from "react";const Home = () = { return (/* Component */);};export default Home;

    Como ya hemos concluido, estos componentes se incluyen en el paquete principal de forma predeterminada (dependiendo del marco y las herramientas de compilación), lo que significa que todo se carga independientemente de la ruta en la que llegue el usuario. Tanto el panel como el componente Acerca de se cargan en la ruta de la página de inicio, etc.

    ( Vista previa grande )

    Refactoricemos nuestras importdeclaraciones como en el ejemplo anterior y usemos lazyla importación para codificar componentes de página dividida. También necesitamos anidar estos componentes en un solo Suspensecomponente. Si tuviéramos que proporcionar un elemento alternativo diferente para estos componentes, anidaríamos cada componente en un Suspensecomponente separado. Los componentes tienen una exportación predeterminada, por lo que no es necesario cambiarlos.

    import { Routes, Route } from "react-router-dom";import React, { lazy, Suspense } from "react";const Dashboard = lazy(() = import("./pages/Dashboard"));const Home = lazy(() = import("./pages/Home"));const About = lazy(() = import("./pages/About"));function App() { return ( Suspense fallback={null} Routes Route path="/" element={Home /} / Route path="/dashboard" element={Dashboard /} / Route path="/about" element={About /} / /Routes /Suspense );}export default App;

    And that’s it! Page components are neatly split into separate packages and are loaded on-demand as the user navigates between the pages. Keep in mind, that you can provide a fallback component like a spinner or a skeleton loader to provide a better loading experience on slower networks and average to low-end devices.

    (Large preview)
    If not addressed on time, bundle size issues get increasingly difficult and risky to fix and refactor on larger projects like this one (filenames and components omitted on purpose). (Large preview)

    Being tasked with optimizing the performance of the entire web app may be a bit overwhelming at first. A good place to start is to audit the app using Webpack Bundle Analyzer or Source Map Explorer and identify bundles that should be code-split and fit the aforementioned criteria. An additional way of identifying those bundles is to run a performance test in a browser or use WebPageTest, and check which bundles block the CPU main thread the longest.

     

    After identifying code-splitting candidates, we need to check the scope of changes that are required to code-split this component from the main bundle. At this point, we need to evaluate if the benefit of code-splitting outweighs the scope of changes required and the development and testing time investment. This risk is minimal to none early in the development cycle.

    Finally, we need to verify that the component has been code-split correctly and that the main bundle size has decreased. We also need to build and test the component to avoid introducing potential issues.

    There are a lot of steps for code-splitting a single existing component, so let’s summarize the steps in a quick checklist:

    1. Audit the site using bundle analyzer and browser performance profiler, and identify larger components and bundles that take the most time to execute.
    2. Check if the benefit of code-splitting outweighs the development and testing time required.
    3. If the component has a named export, convert it to the default export.
    4. If the component is a part of barrel export, remove it from the barrel file.
    5. Refactor import statements to use lazy statements.
    6. Wrap code-split components in the Suspense component and provide a fallback.
    7. Evaluate the resulting bundle (file size and performance gains). If the bundle doesn’t significantly decrease the bundle file size or improve performance, undo code-splitting.
    8. Check if the project builds successfully and if it performs without any issues.

    Performance Budgets

    We can configure our build tools and continuous integration (CI) tools to catch bundle sizing issues early in development by setting performance budgets that can serve as a performance baseline or a general asset size limit. Build tools like Webpack, CI tools, and performance audit tools like Lighthouse can use the defined performance budgets and throw a warning if some bundle or resource goes over the budget limit. We can then run code-splitting for bundles that get caught by the performance budget monitor. This is especially useful information for pull request reviews, as we check how the added features affect the overall bundle size.

    Tools like bundlesizes can be easily integrated with any build or CI tool to keep track of bundle size stats on pull request basis. (Image from bundlesizes documentation) (Large preview)

    We can fine-tune performance budgets to tailor for worse possible user scenarios, and use that as a baseline for performance optimization. For example, if we use the scenario of a user browsing the site on an unreliable and slow connection on an average phone with a slower CPU as a baseline, we can provide optimal user experience for a much wider range of user devices and network types.

    Alex Russell has covered this topic in great detail in his article on the topic of real-world web performance budgets and found out that the optimal budget size for those worst-case scenarios lies somewhere between 130kB and 170kB.

    “Performance budgets are an essential but under-appreciated part of product success and team health. Most partners we work with are not aware of the real-world operating environment and make inappropriate technology choices as a result. We set a budget in time of = 5 seconds first-load Time-to-Interactive and = 2s for subsequent loads. We constrain ourselves to a real-world baseline device + network configuration to measure progress. The default global baseline is a ~$200 Android device on a 400Kbps link with a 400ms round-trip-time (“RTT”). This translates into a budget of ~130-170KB of critical-path resources, depending on composition — the more JS you include, the smaller the bundle must be.”

     

    — Alex Russell

    React Suspense And Server-Side Rendering (SSR)

    An important caveat that we have to be aware of is that React Suspense component is only for client-side use, meaning that server-side rendering (SSR) will throw an error if it tries to render the Suspense component regardless of the fallback component. This issue will be addressed in the upcoming React version 18. However, if you are working on a project running on an older version of React, you will need to address this issue.

    One way to address it is to check if the code is running on the browser which is a simple solution, if not a bit hacky.

    const isBrowser = typeof window !== "undefined"return ( {isBrowser componentLoadCondition ( Suspense fallback={Loading /} SomeComponent / Suspense )} /)

    However, this solution is far from perfect. The content won’t be rendered server-side which is perfectly fine for modals and other non-essential content. Usually, when we use SSR, it is for improved performance and SEO, so we want content-rich components to render into HTML, thus crawlers can parse them to improve search result rankings.

    Until React version 18 is released, React team recommends using the Loadable Components library for this exact case. This plugin extends React’s lazy import and Suspense components, and adds Server-side rendering support, dynamic imports with dynamic properties, custom timeouts, and more. Loadable Components library is a great solution for larger and more complex React apps, and the basic React code-splitting is perfect for smaller and some medium apps.

    Benefits And Caveats Of Code-Splitting

    Hemos visto cómo se pueden mejorar el rendimiento de la página y los tiempos de carga cargando dinámicamente paquetes de JavaScript costosos y no críticos. Como beneficio adicional de la división de código, cada paquete de JavaScript obtiene su hash único, lo que significa que cuando la aplicación se actualiza, el navegador del usuario descargará solo los paquetes actualizados que tengan diferentes hashes.

    Sin embargo, se puede abusar fácilmente de la división de código y los desarrolladores pueden volverse demasiado entusiastas y crear demasiados micropaquetes que perjudican la usabilidad y el rendimiento. Cargar dinámicamente demasiados componentes más pequeños e irrelevantes puede hacer que la interfaz de usuario no responda y se retrase, lo que perjudica la experiencia general del usuario. Una división excesiva del código puede incluso perjudicar el rendimiento en los casos en que los paquetes se sirven a través de HTTP 1.1, que carece de multiplexación .

    Utilice presupuestos de rendimiento, analizadores de paquetes y herramientas de seguimiento del rendimiento para identificar y evaluar a cada candidato potencial para la división de código. Utilice la división de código de forma sensata y moderada, sólo si da como resultado una reducción significativa del tamaño del paquete o una mejora notable del rendimiento.

    Referencias

    Mejora del rendimiento del paquete JavaScript con división de código

    Mejora del rendimiento del paquete JavaScript con división de código

    SmashingConf Nueva York 2024 Índice Errores de agrupación

    programar

    es

    https://aprendeprogramando.es/static/images/programar-mejora-del-rendimiento-del-paquete-javascript-con-division-de-codigo-1130-0.jpg

    2024-04-04

     

    Mejora del rendimiento del paquete JavaScript con división de código
    Mejora del rendimiento del paquete JavaScript con división de código

    Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

     

     

    Top 20