📜 ⬆️ ⬇️

Responsible JavaScript Development Part 2

In April of this year, we published a translation of the first material from a series devoted to a responsible approach to JavaScript development. There, the author reflected on modern web technologies and their rational use. Now we offer you a translation of the second article from this series. It focuses on some technical details regarding the design of web projects.



I have an idea


You and your team have enthusiastically promoted the idea of ​​a complete overhaul of the company's aging website. Your requests reached the leadership, even came into the view of those at the very top. You were given a green light. Your team enthusiastically set to work, attracting designers, copywriters and other specialists to it. Soon you rolled out a new code.

Work on it began innocently. The npm install command is here, the npm install command is there. As soon as you looked around, production dependencies were already being established as if the project development was a wild booze, and you are the one who does not care at all about what will be tomorrow morning.
')
Then you started.

But, unlike the consequences of the craziest drinking party, the terrible thing did not begin the next morning. Unfortunately - not the next morning. Reckoning came in months. She took an unpleasant form of mild nausea and headache for company owners and middle managers who wondered why, after the launch of the new site, conversions and revenues fell. Then the disaster gained momentum. This happened when the technical director returned from the weekend, which he spent somewhere outside the city. He wondered why the company’s website was loading so slowly (if at all) on his phone.

It used to be good for everyone. Now other dark times have come. Meet your first hangover after consuming a large dose of JavaScript.

This is not your fault


While you were trying to cope with a hellish hangover, words like “I told you so” would sound like a well-deserved reprimand to you. And if you were able to fight at that time, they could serve as an occasion for a fight.

When it comes to the consequences of reckless use of JavaScript, you can blame everything and everyone. But looking for the guilty is a waste of time. The modern web device itself requires companies to solve problems faster than their competitors. Such pressure means that we, striving to increase our productivity as much as possible, are likely to grab onto anything. This means that we, with a high degree of probability (although this cannot be called inevitable), will create applications in which there will be many excesses, and, most likely, we will use patterns that harm the performance and availability of applications.

Web development is not an easy task. This is a long job. It is rarely performed well on the first try. The best thing about this work, however, is that we don’t have to do everything perfectly at the very beginning. We can make improvements to projects after they are launched, and, in fact, this material is dedicated to this, the second in a series of articles on a responsible approach to JS development. Perfection is a very distant goal. In the meantime, let's deal with the JavaScript hangover by improving, so to speak, scripting
on the site in the near future.

We deal with common problems


This may seem like a mechanical approach to solving problems, but first go through a list of typical troubles and ways to deal with them. In large development teams, things like this are often forgotten. This is especially true for those teams that work with multiple repositories or do not use an optimized template for their projects.

â–Ť Apply tree shaking algorithm


First, check if your tools are configured to implement the tree shaking algorithm. If you have not come across this concept before, take a look at this material of mine , written last year. If we explain the operation of this algorithm in a nutshell, then we can say that thanks to its use in the production assembly of the application, those packages that, although imported into the project, are not used in it, are not included.

Implementing the tree shaking algorithm is a standard feature of modern bundlers - such as webpack , Rollup, or Parcel . Grunt or gulp are task managers. They do not do this. The task manager, unlike the bundler, does not create a dependency graph . The task manager is engaged, using the necessary plug-ins, performing separate manipulations on the files transferred to it. The functionality of task managers can be expanded using plug-ins, giving them the ability to process JavaScript using bundlers. If expanding the capabilities of the task manager in this direction seems to be a problem, then you probably need to manually check the code base and remove unused code from it.

In order for the tree shaking algorithm to work efficiently, the following conditions must be met:

  1. Application code and installed packages should be presented as ES6 modules . Using the tree shaking algorithm for CommonJS modules is almost impossible.
  2. Your bundler should not transform ES6 modules into modules of some other format during the build of the project. If this happens in the toolchains that use Babel, then @ Babel / present-env must have the modules: false setting. This will cause the ES6 code to not be converted to code that uses CommonJS.

If suddenly, when building your project, the tree shaking algorithm is not applied, the inclusion of this mechanism can improve the situation. Of course, the effectiveness of this algorithm varies from project to project. In addition, the possibility of its use depends on whether the imported modules have side effects . This may affect the ability of the bundler to get rid of the inclusion of unnecessary imported modules in the assembly.

â–ŤDivide the code into parts


It is very likely that you are already using some form of code separation . However, you should check how exactly this is done. Regardless of how you separate the code, I want to offer you to ask yourself the following two very valuable questions:

  1. Do you remove duplicate code from input points ?
  2. Do you lazily load everything that can be loaded in this way with dynamic imports ?

These issues are important because reducing the amount of redundant code is a crucial performance element. Lazy loading the code also improves performance by reducing the amount of JavaScript code that is included in the page and loaded when it is loaded. If we talk about the analysis of the project for the presence of redundant code in it, then for this you can use some kind of tool like the Bundle Buddy. If your project has a problem with this, this tool will let you know about it.


The Bundle Buddy tool can check webpack compilation information and find out how much of the same code is used in your bundles

If we talk about lazy loading of materials, then figuring out where to look for opportunities to apply this optimization may be of some difficulty. When I research an existing project for the possibility of using lazy loading, I look in the code base for those places that involve user interactions with the code. It can be, for example, mouse or keyboard event handlers, as well as other similar things. Any code that requires some user action to run is a good candidate for applying the dynamic import() command to it.

Of course, loading scripts on demand carries the risk of noticeable delays in moving the system to interactive mode. Indeed, before the program can interact with the user interactively, you need to download the appropriate script. If the amount of data transferred does not bother you, consider using the rel = prefetch resource hint to load such scripts with low priority. Such resources will not compete for bandwidth with critical resources. If the user's browser supports rel=prefetch , using this tooltip will only be beneficial. If not, nothing bad will happen, as browsers simply ignore markup that they do not understand.

â–ŤUse the webpack externals option to mark resources located on foreign servers


Ideally, you should host as many dependencies of your site as possible on your own servers. If, for some reason, you, without options, have to download dependencies from other people's servers - put them in the externals block in the webpack settings. If this is not done, this may mean that visitors to your site will download both the code that you host and the same code from other people's servers.

Take a look at a hypothetical situation in which something like this could harm your resource. Suppose your site downloads the Lodash library from a public CDN resource. You also installed Lodash in the project for local development purposes. However, if you do not specify in the webpack settings that Lodash is an external dependency, then your production code will load the library from the CDN, but at the same time it will be included in the bundle that is hosted on your server.

If you are well acquainted with bundlers, then all this may seem to you commonplace truths. But I saw how these things are not paying attention. Therefore, do not take the time to double check your project for the above problems.

If you do not consider it necessary to host your dependencies created by third-party developers yourself, then consider using dns-prefetch , preconnect , or perhaps even preload hints with them. This can reduce the TTI (Time To Interactive, site time to first interactivity) score. And if JavaScript capabilities are required to display the contents of the site, then the site’s Speed ​​Index is also needed.

Smaller alternative libraries and reduced overhead on user systems


What is called " Userland JavaScript " (user-developed JS libraries) seems like an obscene huge candy store. All this open-source magnificence and diversity inspires us, developers, with awe. Frameworks and libraries allow us to expand our applications, quickly equipping them with features that help solve a variety of problems. If we had to implement the same functionality on our own, it would take a lot of time and energy.

Although I personally advocate aggressive minimization of the use of client frameworks and libraries in my projects, I cannot but recognize their great value and usefulness. But, despite this, when it comes to installing new dependencies in the project, we should treat each of them with a fair amount of suspicion. If we have already created and launched something, the operation of which depends on many installed dependencies, then this means that we put up with the additional load on the system that all this creates. It is likely that only package developers can deal with this problem by optimizing their development. Is it so?

Perhaps this is so, but perhaps not. It depends on the dependencies used. For example, React is an extremely popular library. But Preact is a very small alternative to React, which gives the developer almost the same APIs and remains compatible with many React add-ons. Luxon and date-fns are alternatives to moment.js , much more compact than this library, which is not so small .

In libraries like Lodash, you can find many useful methods. But some of them are easy to replace with standard ES6 methods. For example, the Lodash compact method can be replaced with the standard filter arrays method. Many other Lodash methods can also be easily replaced with standard ones. The advantage of this replacement is that we get the same features as using the library, but get rid of a rather large dependency.

Whatever you use, the general idea remains the same: ask if your choice has more compact alternatives. Find out if you can solve the same problems with standard language tools. You may find yourself pleasantly surprised at how little you have to make efforts to seriously reduce the size of the application and the amount of unnecessary load that it exerts on user systems.

Use script differential loading technologies


Chances are Babel is in your toolchain. This tool is used to transform ES6-compliant source code into code that legacy browsers can run. Does this mean that we are doomed to give huge bundles even to browsers that do not need them, until all old browsers simply disappear? Of course not ! Differential resource loading helps circumvent this problem by creating two different assemblies based on ES6 code:


In order to use the technology of differential loading of assemblies, you have to work a little. I will not go into details here - I’ll give a better link to my material, which discusses one of the ways to implement this technology. The essence of all this is that you can modify your build configuration so that during the build of the project an additional version of the JS bundle of your site is created. This additional bundle will be smaller than the main one. It will be intended only for modern browsers. The best part is that this approach allows you to optimize the size of the bundle and at the same time not to sacrifice absolutely nothing of the project’s capabilities. Depending on the application code, the savings on bundle size can be quite significant.


Analysis of bundles for legacy browsers (left) and bundles for new browsers (right). The bundle study was done using webpack-bundle-analyzer. Here is the full size version of this image.

It’s easiest to give different bundles to different browsers using the following trick. It works well in modern browsers:

 <!--     : --> <script type="module" src="/js/app.mjs"></script> <!--     : --> <script defer nomodule src="/js/app.js"></script> 

Unfortunately, this approach has disadvantages. Outdated browsers like IE11, and even relatively modern ones such as Edge versions 15-18, will load both bundles. If you are ready to put up with it, then use this technique and don’t worry about anything.

On the other hand, you need to come up with something in case you are concerned about the impact on the performance of your application of the fact that older browsers have to download both bundles. Here is one potential solution to this problem that uses script injection (instead of the <script> tag we used above). It avoids the double loading of bundles by appropriate browsers. Here is what we are talking about:

 var scriptEl = document.createElement("script"); if ("noModule" in scriptEl) {  //     scriptEl.src = "/js/app.mjs";  scriptEl.type = "module"; } else {  //     scriptEl.src = "/js/app.js";  scriptEl.defer = true; // type="module"   ,    . } //  ! document.body.appendChild(scriptEl); 

This script assumes that if the browser supports the nomodule attribute in the script element, then it understands the type="module" construct. This ensures that legacy browsers will only receive scripts designed for them, and modern ones will receive scripts designed for them. However, keep in mind that dynamically embedded scripts are loaded asynchronously by default. Therefore, if the order of loading dependencies is important for you, set the async attribute to false .

Convey less


I'm not going to attack Babel here. This tool is necessary in modern web development, but it is a very wayward entity. Babel adds a lot of things to the code that it generates that the developer might not know about. Therefore, you will not regret if you look into the bowels of Babel and find out what exactly he is doing. In particular, knowledge of Babel’s internal mechanisms makes it clear that small changes to how someone writes code can have a positive effect on what Babel generates.


Convey less

Namely - that’s what we are talking about. For example, default options are a very handy feature of ES6 that you may already be using:

 function logger(message, level = "log") {  console[level](message); } 

Here it is worth paying attention to the level parameter, whose default value is the string log . This means that if we want to call console.log using the logger wrapper function, then we do not need to pass level to this function. Convenient, right? All this is good - except for what code Babel gets when transforming this function:

 function logger(message) {  var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "log";  console[level](message); } 

This is an example of how, despite the fact that we are guided by good intentions, the comforts that Babel gives can turn into negative consequences. What was just a few characters in the source code turned into a much longer construction in the production version of the program. - , , arguments .

, , ? , Babel :

 //   function logger(...args) {  const [level, message] = args;  console[level](message); } //   Babel function logger() {  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {    args[_key] = arguments[_key];  }  const level = args[0],        message = args[1];  console[level](message); } 

, Babel @babel/preset-env , . , , , , , ! — «» ( true loose ). — , , , , , . «» , Babel , .

, «» , , Babel :

 // Babel      function logger(message, level) {  console[level || "log"](message); } 

, — JavaScript, , . spread , , .

— :

  1. — @babel/runtime @babel/plugin-transform-runtime , , Babel .
  2. , . @babel/polyfill . , babel /preset-env useBuiltins usage .

, , , , , . , JSX , , , . , , . , , . , Babel — . , Babel. , .

: —


, . , JavaScript-, , . , , . - . . , , , , , , , .

, , , , , , , . - — . , , , . . , , , , . , , , .

Dear readers! JS-?

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


All Articles