Sacudida de árboles
Sacudida de árboles es un término comúnmente utilizado en el contexto JavaScript para la eliminación de código muerto. Se basa en la estructura estática de la sintaxis del módulo ES2015, es decir, import
y export
. El nombre y el concepto han sido popularizados por el acumulador de módulos ES2015.
La versión de webpack 2 viene con soporte incorporado para módulos ES2015 (alias módulos harmony), así como detección de exportación de módulos no utilizados. La nueva versión de webpack 4 amplía esta capacidad con una forma de proporcionar sugerencias al compilador a través de la propiedad "sideEffects"
package.json
para indicar qué archivos de su proyecto son «puros» y, por lo tanto, seguros de podar si no se usan.
Agregar una utilidad
Agreguemos un nuevo archivo de utilidad a nuestro proyecto, src/math.js
, que exporta dos funciones:
project
webpack-demo|- package.json|- webpack.config.js|- /dist |- bundle.js |- index.html|- /src |- index.js+ |- math.js|- /node_modules
src/math.js
export function square(x) { return x * x;}export function cube(x) { return x * x * x;}
Establezca la opción de configuración mode
en desarrollo para asegurarse de que el paquete no esté minificado:
webpack.config.js
const path = require('path');module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), },+ mode: 'development',+ optimization: {+ usedExports: true,+ },};
Con eso en su lugar, actualicemos nuestro script de entrada para utilizar uno de estos nuevos métodos y eliminemos lodash
para simplificar:
src/index.js
- import _ from 'lodash';+ import { cube } from './math.js'; function component() {- const element = document.createElement('div');+ const element = document.createElement('pre');- // Lodash, now imported by this script- element.innerHTML = _.join(, ' ');+ element.innerHTML = .join('\n\n'); return element; } document.body.appendChild(component());
tenga en cuenta que nosotros no import
square
método de la etiqueta src/math.js
módulo. Esa función es lo que se conoce como «código muerto», lo que significa un export
no utilizado que debe eliminarse. Ahora ejecutemos nuestro script npm, npm run build
, e inspeccionemos el paquete de salida:
dist / bundle.js (alrededor de las líneas 90 – 100)
/* 1 *//***/ (function (module, __webpack_exports__, __webpack_require__) { 'use strict'; /* unused harmony export square */ /* harmony export (immutable) */ __webpack_exports__ = cube; function square(x) { return x * x; } function cube(x) { return x * x * x; }});
Observe el comentario anterior unused harmony export square
. Si observa el código debajo de él, notará que square
no se está importando, sin embargo, aún está incluido en el paquete. Lo arreglaremos en la siguiente sección.
Marque el archivo como sin efectos secundarios
En un mundo de módulos ESM al 100%, identificar los efectos secundarios es sencillo. Sin embargo, todavía no estamos allí, por lo que mientras tanto es necesario proporcionar sugerencias al compilador de webpack sobre la «pureza» de su código.
La forma de lograrlo es el paquete "sideEffects"
.propiedad json.
{ "name": "your-project", "sideEffects": false}
Todo el código mencionado anteriormente no contiene efectos secundarios, por lo que simplemente podemos marcar la propiedad como false
para informar a webpack de que puede podar de forma segura las exportaciones no utilizadas.
Si su código tuvo algunos efectos secundarios, sin embargo, se puede proporcionar una matriz:
{ "name": "your-project", "sideEffects": }
La matriz acepta patrones glob simples para los archivos relevantes. Utiliza glob-a-regexp bajo el capó (Compatible con: *
**
{a,b}
). Patrones como
*.css
, que no incluyen un /
, será tratada como **/*.css
.
{ "name": "your-project", "sideEffects": }
Finalmente, "sideEffects"
también se puede configurar desde la opción de configuración module.rules
.
Clarificación de sacudidas de árboles y efectos secundarios
Las optimizaciones sideEffects
y usedExports
(más conocidas como sacudidas de árboles) son dos cosas diferentes.
sideEffects
es mucho más eficaz ya que permite omitir módulos/archivos completos y el subárbol completo.
usedExports
se basa en terser para detectar efectos secundarios en declaraciones. Es una tarea difícil en JavaScript y no tan efectiva como la sencilla banderasideEffects
. Tampoco puede omitir subárbol/dependencias, ya que la especificación dice que los efectos secundarios deben evaluarse. Mientras que la función de exportación funciona bien, los Componentes de Orden Superior (HOC) de React son problemáticos en este sentido.
Hagamos un ejemplo:
import { Button } from '@shopify/polaris';
La versión pre-incluida se ve así:
import hoistStatics from 'hoist-non-react-statics';function Button(_ref) { // ...}function merge() { var _final = {}; for ( var _len = arguments.length, objs = new Array(_len), _key = 0; _key < _len; _key++ ) { objs = arguments; } for (var _i = 0, _objs = objs; _i < _objs.length; _i++) { var obj = _objs; mergeRecursively(_final, obj); } return _final;}function withAppProvider() { return function addProvider(WrappedComponent) { var WithProvider = /*#__PURE__*/ (function (_React$Component) { // ... return WithProvider; })(Component); WithProvider.contextTypes = WrappedComponent.contextTypes ? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes) : polarisAppProviderContextTypes; var FinalComponent = hoistStatics(WithProvider, WrappedComponent); return FinalComponent; };}var Button$1 = withAppProvider()(Button);export { // ..., Button$1,};
Cuando Button
no se usa, puede eliminar de manera efectiva el export { Button$1 };
que deja todo el código restante. Por lo tanto, la pregunta es: «¿Tiene este código efectos secundarios o se puede eliminar de forma segura?». Es difícil de decir, especialmente por esta línea withAppProvider()(Button)
. se llama a withAppProvider
y también se llama al valor devuelto. ¿Hay algún efecto secundario al llamar a merge
o hoistStatics
? ¿Hay efectos secundarios al asignar WithProvider.contextTypes
(Setter?) o al leer WrappedComponent.contextTypes
(Getter?).
Terser en realidad intenta averiguarlo, pero no lo sabe con certeza en muchos casos. Esto no significa que terser no esté haciendo bien su trabajo porque no pueda entenderlo. Es demasiado difícil determinarlo de forma fiable en un lenguaje dinámico como JavaScript.
Pero podemos ayudar a terser usando la anotación /*#__PURE__*/
. Marca una declaración como libre de efectos secundarios. Por lo tanto, un simple cambio haría posible sacudir el código en árbol:
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
Esto permitiría eliminar este fragmento de código. Pero todavía hay preguntas con las importaciones que deben incluirse/evaluarse porque podrían contener efectos secundarios.
Para hacer frente a esto, utilizamos la etiqueta "sideEffects"
propiedad package.json
.
Es similar a /*#__PURE__*/
pero a nivel de módulo en lugar de a nivel de instrucción. Dice ("sideEffects"
propiedad): «Si no se utiliza ninguna exportación directa desde un módulo marcado con efectos secundarios sin efectos, el bundler puede omitir la evaluación del módulo de efectos secundarios.».
En el ejemplo de Polaris de Shopify, los módulos originales se ven así:
index.js
import './configure';export * from './types';export * from './components';
componentes/index.paquete js
// ...export { default as Breadcrumbs } from './Breadcrumbs';export { default as Button, buttonFrom, buttonsFrom } from './Button';export { default as ButtonGroup } from './ButtonGroup';// ...
.json
// ..."sideEffects": ,// ...
Para import { Button } from "@shopify/polaris";
esto tiene las siguientes implicaciones:
- incluir: incluir el módulo, evaluar y seguir analizando las dependencias
- saltar: no lo incluye, no evaluarlo, pero continuar con el análisis de las dependencias
- excluir de ella: no lo incluye, no la valore y no analizar las dependencias
Específicamente por la coincidencia de recurso(s):
-
index.js
: No exportación directa se utiliza, pero marcado con efectos colaterales -> incluir -
configure.js
: No a la exportación, sino que se marcan con efectos colaterales -> incluir -
types/index.js
: exportación se utiliza, no se marcan con efectos colaterales -> excluir -
components/index.js
: No hay exportación directa se utiliza, no se marcan con los efectos secundarios, pero reexportados las exportaciones se utilizan -> saltar -
components/Breadcrumbs.js
: No se usa exportación, no se marca con Efectos laterales – > excluirlo. Esto también excluye todas las dependencias comocomponents/Breadcrumbs.css
incluso si están marcadas con efectos laterales. -
components/Button.js
: exportación Directa se utiliza, no se marcan con efectos colaterales -> incluir -
components/Button.css
: exportación se utiliza, pero marcado con efectos colaterales -> incluir
En este caso sólo 4 módulos están incluidos en el paquete:
-
index.js
: bastante vacío configure.js
components/Button.js
components/Button.css
Después de esta optimización, otras optimizaciones se pueden aplicar. Por ejemplo: buttonFrom
y buttonsFrom
las exportaciones de Button.js
no se usan demasiado. usedExports
optimization lo recogerá y terser podrá soltar algunas instrucciones del módulo.También se aplica la concatenación de módulos
. Para que estos 4 módulos más el módulo de entrada (y probablemente más dependencias) se puedan concatenar. index.js
no tiene código generado al final.
Marcar una llamada de función como libre de efectos secundarios
Es posible indicar a webpack que una llamada de función está libre de efectos secundarios (pura) utilizando la anotación /*#__PURE__*/
. Se puede colocar delante de las llamadas de función para marcarlas como libres de efectos secundarios. Los argumentos pasados a la función no están marcados por la anotación y es posible que deban marcarse individualmente. Cuando el valor inicial en una declaración de variable de una variable no utilizada se considera libre de efectos secundarios (puro), se marca como código muerto, no se ejecuta y el minimizador lo elimina. Este comportamiento se habilita cuando optimization.innerGraph
se establece en true
.archivo
.js
/*#__PURE__*/ double(55);
Comprimir la Salida de
Entonces, hemos complementado nuestro «código muerto» a dejarse caer por el uso de la etiqueta import
y export
sintaxis, pero todavía tenemos que caer de la agrupación. Para ello, establezca la opción de configuración mode
en production
.
webpack.config.js
const path = require('path');module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), },- mode: 'development',- optimization: {- usedExports: true,- }+ mode: 'production',};
Con eso al cuadrado, podemos ejecutar otro npm run build
y ver si algo ha cambiado.
¿Nota algo diferente endist/bundle.js
? Claramente, todo el paquete ahora está minificado y destrozado, pero, si lo observa con cuidado, no verá la función square
incluida, sino que verá una versión destrozada de la función cube
function r(e){return e*e*e}n.a=r
). Con minificación y agitación de árboles, nuestro paquete ahora es unos pocos bytes más pequeño. Si bien esto puede no parecer mucho en este ejemplo artificial, la sacudida de árboles puede producir una disminución significativa en el tamaño del paquete cuando se trabaja en aplicaciones más grandes con árboles de dependencias complejos.
Conclusión
Entonces, lo que hemos aprendido es que para aprovechar el temblor de árboles, debes hacerlo…
- Utilice la sintaxis del módulo ES2015 (es decir,
import
yexport
). - Asegúrese de que ningún compilador transforme su sintaxis de módulo ES2015 en módulos CommonJS (este es el comportamiento predeterminado del popular preset de Babel @babel/preset-env – consulte la documentación para obtener más detalles).
- Agregue una propiedad
"sideEffects"
al archivopackage.json
de su proyecto. - Utilice la opción de configuración
production
mode
para habilitar varias optimizaciones, incluida la minificación y la agitación de árboles.
Puede imaginar su aplicación como un árbol. El código fuente y las bibliotecas que utiliza representan las hojas verdes y vivas del árbol. El código muerto representa las hojas marrones y muertas del árbol que se consumen en otoño. Para deshacerse de las hojas muertas, tienes que sacudir el árbol, haciendo que caigan.
Si está interesado en más formas de optimizar su producción, vaya a la siguiente guía para obtener detalles sobre la construcción para producción.