📜 ⬆️ ⬇️

Make Modern Build

Hi, Habr!

Every modern browser now allows you to work with ES6 Modules .

At first glance it seems that this is a completely useless thing - after all, we all use collectors who replace imports with their internal challenges. But if you delve into the specifications, it turns out that thanks to them you can bring up a separate assembly for modern browsers.
')
Under the cut there is a story about how I was able to reduce the size of the application by 11% without damage to old browsers and my nerves.



ES6 Modules Features


ES6 Modules is a well-known and widely used modular system:

/* someFile.js */ import { someFunc } from 'path/to/helpers.js' 

 /* helpers.js */ export function someFunc() { /* ... */ } 

To use this modular system in browsers, you must add the type module to each script tag. Older browsers will see that the type is different from text / javascript, and will not execute the file as JavaScript.

 <!--        ES6 Modules --> <script type="module" src="/path/to/someFile.js"></script> 

The specification still has the nomodule attribute for script tags. Browsers that support ES6 Modules will ignore this script, and older browsers will download it and execute it.

 <!--       --> <script nomodule src="/path/to/someFileFallback.js"></script> 

It turns out, you can simply make two assemblies: the first with the module type for modern browsers (Modern Build), and the other with the nomodule for the old (Fallback build):

 <script type="module" src="/path/to/someFile.js"></script> <script nomodule src="/path/to/someFileFallback.js"></script> 

Why do you need it


Before sending a project to production, we must:


In my projects I try to support the maximum number of browsers, sometimes even IE 10 . Therefore, my list of polifilov consists of such basic things as es6.promise, es6.object.values, etc. But browsers with support for ES6 Modules have all the ES6 methods, and they do not need extra kilobytes of polyfiles.

Transpilation also leaves a significant mark on file size: to cover most browsers, babel / preset-env uses 25 transformers, each of which increases the size of the code. At the same time, for browsers with support for ES6 Modules, the number of transformers is reduced to 9.

So, in the assembly for modern browsers, we can remove unnecessary polifila and reduce the number of transformers, which will greatly affect the size of the final files!

How to add polifila


Before we go to prepare Modern Build for modern browsers, it is worth mentioning how I add polyfiles to the project.



Typically, projects use core-js to add all possible polyfiles.

Of course, you do not want all 88 Kb polyfiles from this library, but only those that are needed for your browserslist. This feature is available using babel / preset-env and its useBuiltIns option. If you set the entry value for it, then the core-js import will be replaced with the imports of the individual modules needed by your browsers:

 /* .babelrc.js */ module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'entry', /* ... */ }] ], /* ... */ }; 

 /*   */ import 'core-js'; 

 /*   */ import "core-js/modules/es6.array.copy-within"; import "core-js/modules/es6.array.fill"; import "core-js/modules/es6.array.find"; /*   -  */ 

But with such a transformation, we got rid of only a part of unnecessary very old polyfiles. We still have polyfiles for TypedArray, WeakMap and other strange things that are never used in the project.

To completely defeat this problem, for the useBuiltIns option, I set the usage value. At the compilation stage, babel / preset-env will analyze the files for using features that are missing in the selected browsers, and add polyfiles to them:

 /* .babelrc.js */ module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', /* ... */ }] ], /* ... */ }; 

 /*   */ function sortStrings(strings) { return strings.sort(); } function createResolvedPromise() { return Promise.resolve(); } 

 /*   */ import "core-js/modules/es6.array.sort"; import "core-js/modules/es6.promise"; function sortStrings(strings) { return strings.sort(); } function createResolvedPromise() { return Promise.resolve(); } 

In the example above, babel / preset-env added a polyfill to the sort function. In JavaScript, it is impossible to find out what type of object will be passed to the function - it will be an array or a class object with the sort function, but babel / preset-env chooses the worst script for itself and inserts a poly file.

Situations when babel / preset-env makes mistakes happen all the time. To remove unnecessary polyfiles, check which ones you import from time to time and remove the extra ones with the exclude option:

 /* .babelrc.js */ module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', //   ,  ,     debug: true, //      exclude: ['es6.regexp.to-string', 'es6.number.constructor'], /* ... */ }] ], /* ... */ }; 

I do not consider the regenerator-runtime module, since I use fast-async ( and I advise everyone ).

Create Modern Build


Let's start setting up the Modern Build.

Make sure that we have a browserslist file in the project that describes all the necessary browsers:

 /* .browserslistrc */ > 0.5% IE 10 

Add the BROWSERS_ENV environment variable at build time, which can take the values ​​of fallback (for Fallback Build) and modern (for Modern Build):

 /* package.json */ { "scripts": { /* ... */ "build": "NODE_ENV=production webpack /.../", "build:fallback": "BROWSERS_ENV=fallback npm run build", "build:modern": "BROWSERS_ENV=modern npm run build" }, /* ... */ } 

Now change the configuration of babel / preset-env. To specify the supported browsers in the preset there is the targets option. It has a special abbreviation - esmodules. When using it, babel / preset-env will automatically substitute browsers that support ES6 modules .

 /* .babelrc.js */ const isModern = process.env.BROWSERS_ENV === 'modern'; module.exports = { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', //  Modern Build     ES6 modules, //   Fallback Build     .browsersrc targets: isModern ? { esmodules: true } : undefined, /* ... */ }] ], /* ... */ ], }; 

Babel / preset-env will do the rest of the work for us: select only the necessary polyfiles and transformations.

Now we can build a project for modern or old browsers just with a command from the console!

We connect Modern and Fallback Build


The last step is to combine Modern and Fallback Builds into one.

I plan to create such a project structure:

 //     dist/ //  html- index.html //   Modern Build' modern/ ... //   Fallback Build' fallback/ ... 

In index.html there will be links to the necessary javascript files from both assemblies:

 /* index.html */ <html> <head> <!-- ... --> </head> <body> <!-- ... --> <script type="module" src="/modern/js/app.540601d23b6d03413d5b.js"></script> <script nomodule src="/fallback/js/app.4d03e1af64f68111703e.js"></script> </body> </html> 

This step can be divided into three parts:

  1. Build Modern and Fallback Build in different directories.
  2. Getting information about the paths to the required javascript files.
  3. Creating index.html with links to all javascript files.

Getting started!

Building Modern and Fallback Build in different directories


To begin with, we will make the simplest step - we will assemble the Modern and Fallback Build in different directories within the dist directory.

It is impossible to simply specify the desired directory for output.path, since we need the webpack to have paths to the files relative to the dist directory (index.html is in this directory, and all other dependencies will be pumped out relative to it).

Create a special function to generate file paths:

 /* getFilePath.js */ /*   ,       */ const path = require('path'); const isModern = process.env.BROWSERS_ENV === 'modern'; const prefix = isModern ? 'modern' : 'fallback'; module.exports = relativePath => ( path.join(prefix, relativePath) ); 

 /* webpack.prod.config.js */ const getFilePath = require('path/to/getFilePath'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { mode: 'production', output: { path: 'dist', filename: getFilePath('js/[name].[contenthash].js'), }, plugins: [ new MiniCssExtractPlugin({ filename: getFilePath('css/[name].[contenthash].css'), }), /* ... */ ], /* ... */ } 

The project began to gather in different directories for Modern and Fallback Build'a.

Getting information about the paths to the required javascript files


To get information about the collected files, connect the webpack-manifest-plugin. At the end of the build, he will add a file manifest.json with information about the paths to the files:

 /* webpack.prod.config.js */ const getFilePath = require('path/to/getFilePath'); const WebpackManifestPlugin = require('webpack-manifest-plugin'); module.exports = { mode: 'production', plugins: [ new WebpackManifestPlugin({ fileName: getFilePath('manifest.json'), }), /* ... */ ], /* ... */ } 

Now we have information about the collected files:

 /* manifest.json */ { "app.js": "/fallback/js/app.4d03e1af64f68111703e.js", /* ... */ } 

Creating index.html with links to all javascript files


The case remains for the small - add index.html and paste in it the path to the desired files.

To generate the html file, I will use the html-webpack-plugin during the Modern Build. The paths to the html-webpack-plugin modern-files will be inserted by myself, and I will get the paths to the fallback-files from the file created in the previous step and paste them into HTML using a small webpack-plugin:

 /* webpack.prod.config.js */ const HtmlWebpackPlugin = require('html-webpack-plugin'); const ModernBuildPlugin = require('path/to/ModernBuildPlugin'); module.exports = { mode: 'production', plugins: [ ...(isModern ? [ //  html-  Modern Build new HtmlWebpackPlugin({ filename: 'index.html', }), new ModernBuildPlugin(), ] : []), /* ... */ ], /* ... */ } 

 /* ModernBuildPlugin.js */ // Safari 10.1    nomodule. //      Safari   . //    : // https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc const safariFix = '!function(){var e=document,t=e.createE/* ...   ... */'; class ModernBuildPlugin { apply(compiler) { const pluginName = 'modern-build-plugin'; //    Fallback Build const fallbackManifest = require('path/to/dist/fallback/manifest.json'); compiler.hooks.compilation.tap(pluginName, (compilation) => { //    html-webpack-plugin, //      HTML compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(pluginName, (data, cb) => { //  type="module"  modern- data.body.forEach((tag) => { if (tag.tagName === 'script' && tag.attributes) { tag.attributes.type = 'module'; } }); //    Safari data.body.push({ tagName: 'script', closeTag: true, innerHTML: safariFix, }); //  fallback-   nomodule const legacyAsset = { tagName: 'script', closeTag: true, attributes: { src: fallbackManifest['app.js'], nomodule: true, defer: true, }, }; data.body.push(legacyAsset); cb(); }); }); } } module.exports = ModernBuildPlugin; 

Update package.json:

 /* package.json */ { "scripts": { /* ... */ "build:full": "npm run build:fallback && npm run build:modern" }, /* ... */ } 

Using the npm run build: full command, we will create one html file with Modern and Fallback Build. Any browser will now get the JavaScript that it is able to execute.

Add Modern Build to your application


In order to check my decision on something real, I drove it to one of my projects. Configuration took me less than an hour, and the size of the JavaScript files was reduced by 11%. Excellent result with a simple implementation.

Thank you for reading the article to the end!

Used materials


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


All Articles