Tree Shaking Reaccionar aplicaciones nativas

Cómo aplicamos técnicas de optimización comunes a las aplicaciones web a nuestra aplicación React Native para reducir los tiempos de inicio en un 20 %.
¿Árbol qué?
Es cierto que Tree Shaking puede ser un término confuso. Es posible que ya haya oído hablar de él como "elisión de importación" en TypeScript. Tree Shaking es una forma de eliminación de código muerto relacionada específicamente con la eliminación de exportaciones no utilizadas. Si concatenáramos todos los módulos, las exportaciones no utilizadas son efectivamente código muerto y se pueden eliminar. Sin embargo, el proceso de determinar las exportaciones no utilizadas no es trivial. Tree Shaking generalmente se implementa a nivel de compilador/empaquetador (por ejemplo , Webpack o ESBuild ), no mediante un motor de JavaScript (como V8 o Hermes). Muchos patrones en JavaScript pueden romper Tree Shaking, pero en este artículo quiero centrarme en un aspecto: el sistema de módulos. Los dos sistemas de módulos relevantes que necesitamos entender aquí son los módulos CommonJS yMódulos ES .

CommonJS se usa cuando escribes module.exports = {}
o exports.someMethod = () => {}
. Los módulos ES se identifican por import
y export
sintaxis. Es más difícil para los compiladores aplicar Tree Shaking al código usando CommonJS que los módulos ES. Los módulos CommonJS suelen ser dinámicos, mientras que los módulos ES se pueden analizar estáticamente. Por ejemplo, no es trivial detectar estáticamente todos los identificadores de exportación en el siguiente código:
Dado que los módulos ES se pueden analizar estáticamente por diseño, a los compiladores les resulta más fácil detectar exportaciones no utilizadas.
Por lo tanto, le conviene darle a su compilador de optimización módulos ES para trabajar y no módulos CommonJS.
Fondo
Antes de unirme a Klarna, no tenía experiencia trabajando con React Native. Durante una refactorización de rutina, apliqué la siguiente diferencia:
Suponiendo que el paquete usado se consideraría someFeatureMethod
como no utilizado si SOME_STATIC_FLAG
es falso y, por lo tanto, se eliminaría some-feature-module
del paquete final. Durante la revisión del código, esta diferencia se marcó como problemática, así que me senté y verifiqué dos veces mis suposiciones y dónde estaban rotas. Afortunadamente, ya cambiamos a Webpack (en forma de Re.Pack ) un par de meses antes para habilitar la división de paquetes conReact.lazy
. Esto hizo que me resultara más fácil configurar el proceso de agrupación de forma que pudiera inspeccionar el paquete de JavaScript final. En nuestro caso, solo era necesario deshabilitar Hermes para ver el resultado final de JavaScript.
Después de algunas pruebas y errores para que sea más fácil encontrar dónde some-feature-module
se importa, encontré la siguiente línea: c=(n(463526),n(456189)
El operador de coma es algo que normalmente no usa, así que permítame resumir lo que hace: evalúa todos los operandos y solo usa el valor de retorno de el último operando. En otras palabras, el valor de retorno de n(463526)
no se usó. Como ya tenía experiencia trabajando con sacudir árboles en la web, tenía bastante claro qué era esto antes de la minificación: require('some-feature-module')
(Webpack transforma las cadenas de origen de importación en números).
De hecho, Webpack reconoció que no someFeatureMethod
se usó y, por lo tanto, eliminó su uso. Sin embargo, Webpack rescató la eliminación de las exportaciones no utilizadas del módulo y, por lo tanto, mantuvo la importación porque no sabía si el módulo tenía algún efecto secundario. Si un módulo tiene efectos secundarios, no podemos simplemente eliminarlo del paquete, ya que eso cambiaría el flujo del programa.
Todo lo que tuvimos que hacer para que la diferencia original funcionara como se esperaba fue asegurarnos de que Tree Shaking se aplicara al paquete final.
Implementación
Todo se reduce a asegurarse de no transpilar módulos ES a CommonJS antes de que Webpack empaquete todos los módulos. Si está utilizando el ajuste preestablecido Metro Babel (predeterminado para las nuevas aplicaciones React Native), la mayor parte del trabajo se reduce a habilitar disableImportExportTransform
:
Tenga en cuenta que esta opción no está documentada actualmente y puede eliminarse en cualquier momento.
También necesitábamos decirle a Webpack que usara puntos de entrada que usaran módulos ES en lugar de módulos CommonJS. Para archivos individuales, eso significa preferir .mjs
mientras que para paquetes necesitamos decirle a Webpack que use el module
campo principal .
Sin embargo, esto reveló problemas con la forma en que escribimos JavaScript y cómo se escribe el código en el ecosistema React Native. Hemos identificado 3 clases de problemas.
Exportación de sintaxis diferente en main
ymodule
Estos campos principales solo deben usarse para diferenciar el sistema del módulo ( main
para CommonJS, module
para módulos ES). Sin embargo, muchos paquetes envían una sintaxis más moderna desde el punto de module
entrada. Por ejemplo, class
la sintaxis actualmente no es compatible con Hermes .
Por ahora, transpilamos todo node_modules
el contenido a la sintaxis ES5 o, más bien, a la sintaxis compatible con Hermes agregando una configuración personalizada rule
a la configuración del paquete web:
Importación de un módulo CommonJS con sintaxis ambigua
Webpack no podrá encontrar exportaciones de módulos que mezclen sistemas de módulos. Sin embargo, React Native en sí envía sus archivos fuente con un sistema de módulos mixtos, por ejemplo
La solución aquí es seguir transpilando estos módulos a CommonJS (deshabilitando así Tree Shaking) agregando un especial rule
a la configuración de Webpack:
Importar identificadores no existentes
Esto es en realidad un SyntaxError en JavaScript que la mayoría no conoce. Por ejemplo, import { doesNotExist } from 'some-module';
arrojará un SyntaxError
. Es en gran medida una molestia para los desarrolladores, pero puede generar problemas reales de tiempo de ejecución. Aplicamos esta implementación estricta de módulos ES en Webpack habilitando module.parser.javascript.exportsPresence
la configuración de Webpack.
La mayoría de estos problemas fueron causados por la reexportación de tipos en TypeScript, por ejemplo
Afortunadamente, TypeScript puede marcar estos problemas a nivel de tipo habilitando isolatedModules
:
type
los modificadores en los nombres de importación son nuevos en TypeScript 4.5. Agregar soporte para type
modificadores en los nombres de importación fue todo un desafío, ya que necesitábamos actualizar el analizador ESLint que usamos , Prettier y TypeScript.
Agregar type
modificadores para importar nombres da como resultado que Babel elimine las importaciones de tipos que en realidad no existen en tiempo de ejecución.
Resultados
La implementación inicial fue bastante complicada. Sin embargo, los primeros resultados ya mostraron una mejora promedio de inicio del 20 % en ambas plataformas (Samsung Galaxy S9 2.2s por debajo de 2.8s y iPhone 11 640ms por debajo de 802ms).
Lo que vimos fue una reducción del 46 % de nuestra parte inicial crítica de JavaScript. El tamaño total de JavaScript que enviamos se redujo en un 14 %. La diferencia se atribuye en gran medida a mover el código del fragmento principal a fragmentos asíncronos (características y rutas).


Estas imágenes son creadas por el asombroso statoscope.tech que nos ayudó a analizar este cambio y continuará ayudándonos a impulsar mejoras adicionales en el tamaño del paquete.
Tenga en cuenta que la reducción no solo proviene de eliminar la exportación no utilizada, sino también de que Webpack ModuleConcatenationPlugin
puede concatenar más módulos. En otras palabras, podemos elevar el alcance de más módulos . Todavía no estamos utilizando completamente la elevación del alcance. Ahora mismo solo se iza el 20% de los módulos. Esperábamos algo más de tamaño de paquete y ganancias de tiempo de ejecución una vez que aumentamos ese número.
La reducción del 40 % en JavaScript se asigna casi exactamente 1:1 al tiempo que lleva evaluar el paquete de JavaScript antes de que pueda ejecutarse. La evaluación de JavaScript bloquea el tiempo de inicio resultante, por lo que reducir la cantidad de JavaScript enviado reduce directamente el tiempo de inicio.
Después de que se realizó el paso final de la implementación 2 semanas después, todavía teníamos los mismos resultados en nuestro laboratorio y estábamos listos para implementar esta función en nuestra sucursal principal. Tuvimos especial cuidado en realizar el cambio final directamente después de nuestro límite de lanzamiento. Esto nos permitió probar ampliamente el nuevo sistema de módulos en versiones de aplicaciones internas que utilizan nuestros empleados. Después de una semana de pruebas internas, la función se implementó gradualmente para nuestros usuarios finales. Teníamos muchas esperanzas después de ver que la estabilidad de la aplicación no se vio afectada en gran medida. Los datos de producción mostraron las mismas mejoras relativas al tiempo medio de inicio que vimos en los resultados de nuestro laboratorio:
La versión 22.37 no tiene sacudidas de árboles; 22.38 con sacudida de árboles


Estas mejoras tienen el costo de mayores tiempos de construcción. Empaquetar el paquete de JavaScript de producción lleva aproximadamente un 30 % (4 minutos) más de tiempo. Estamos felices de aprovechar estos mayores tiempos de compilación, ya que se traducen directamente en una mejor experiencia del usuario. Algunos de los mayores tiempos de compilación se atribuyen a la transpilación de más de lo necesario. La implementación inicial no dedicó tiempo a reducir la cantidad que necesitamos transpilar. También recuperaremos algunos de los aumentos del tiempo de compilación, cuantos más paquetes envíen los módulos ES adecuados. Tenga en cuenta que el tiempo de agrupación de JavaScript no es la única tarea requerida para crear una aplicación React Native. Con la compilación de archivos binarios, etc., el aumento en la agrupación de JavaScript no es tan impactante al final.
Que sigue
Parece que los módulos ES no se han trabajado activamente en el ecosistema React Native. Querremos alinear más el ecosistema en el uso adecuado de los módulos ES (por ejemplo module
, entradas que apunten a JavaScript con una sintaxis equivalente). De esa manera podemos reducir nuestra configuración de compilación y transpilar menos.
Si bien hay soporte para usar módulos ES detrás de una bandera en Metro ( experimentalImportSupport
), está marcado como experimental y no documentado. Habilitar esa bandera en desarrollo no funciona para nosotros (todavía), pero esperamos que algún día podamos usar el mismo sistema de módulos en desarrollo y producción. Queremos reiniciar la discusión sobre los módulos ES en React native, ya que parece que actualmente no se está trabajando activamente en el soporte para los módulos ES. El soporte de Tree Shaking incluso fue completamente abandonado hace años. .
Después de todo, los módulos ES son una característica del lenguaje que todos los que conocen JavaScript eventualmente también aprenden. No vemos ninguna razón por la que React Native deba tener un paso de aprendizaje adicional para comprender la división de paquetes y la eliminación de código muerto.
¡Escribe una vez, corre a cualquier lugar!
¿Disfrutaste esta publicación? Siga a Klarna Engineering en Medium y LinkedIn para estar al tanto de más artículos como estos.