misc/class
lib/jquery_pnotify, lib/moment, lib/lodash, misc/notification, site/engine, misc/social
if( $.browser.msie && $.browser.version <= 8 ) include('lib/respond'); $._social.__cfg = {"init":[{"service":"basic"},{"fb_app_id":"1997094873850041","service":"fb"},{"vk_app_id":"2978320","service":"vk"},{"service":"twi"}],"like":[{"service":"fb"},{"service":"vk"},{"via":"","channel":"","hash_tag":"","service":"twi"}]}; window._SiteEngine = new classes.SiteEngine( { user_id: 0, controller: 'content_tape', action: 'tag', content_css_version: '1459538664', social_enabled: 0} );

Faiwer

Блог web-программиста

Поиск по метке: Babel

Babel to EsBuild migration for Webpack

9 февраля 2025

Recently I had some time to take a look at the performance of my pet project. One thing bothered me: the project is relatively small, but the bundle size was too big: 780 KiB. Whereas the project had only a few dependencies: react (~10 KiB), react-router (~70 KiB), react-dom (~90 KiB), dayjs (~3 KiB), lodash (~70 KiB). The bundle should not be that big for so few dependencies.

So I ran webpack-dev-analyzer and found a weird thing: the sum of the libs is 3 times smaller than the final bundle size. Then I decided to take a look at @statoscope/webpack-plugin. It was a good tool but it gave me the same result. The bundle has way too much trash. 

Ok. Probably it's just webpack wrappers over 300+ small modules. So I asked Perplexity - what can I do with it? Turned out all the proper options were already activated. So, I made a decision - it's time to try esbuild. It made a huge impact on my work project, so I knew it could work out. 

I knew that esbuild didn't support a lot of stuff that babel supports. So I expected that it would take a day to migrate the codebase to esbuild. But surprisingly once I replaced babel-loader with esbuild-loader the build command worked out. The build didn't work but the bundle was assembled without any errors.

Did it help? Nope. Instead of 780 KiB, I got ~1 MiB. Luckily I found out that there's also EsbuildPlugin, which replaces TerserPlugin and works better. Added this line, it worked like a charm, and... Whoa! 280 KiB. That's huuuge.

So I figured out - it worked. Now I have to find a way to fix the app (spoiler: it didn't work at all). 

Implicit React imports

React 17+ supports the possibility of avoiding adding import React from 'react' to every TSX-file. esbuild supports it too, but somehow it didn't work out of the box. Solution:

  const esbuildLoader: RuleSetRule = {
    test: /\.(js|ts|tsx)$/,
    loader: 'esbuild-loader',
    exclude: /node_modules/,
    options: {
       target: 'es2020',
       minify: cfg.PROD,
+      // Support implicit React imports:
+      jsx: 'automatic',
+      jsxFactory: 'React.createElement',
+      jsxImportSource: 'react',
    } satisfies LoaderOptions,
  };

TSX-Control-Statements

I just removed the packages and all their <If/>s. I liked them though. There's a plugin for Vite that adds their support. But I still use Webpack and the plugin works via string replacements, not via AST. 

SourceMaps

To enable sourcemaps I had to disable EsbuildPlugin for development. Theoretically, it supports sourcemaps, but no matter what I did it didn't work. I even debugged the package itself. Didn't help. So I surrendered and enabled it only for the prod build.

TypeScript

EsBuild supports TypeScript syntax out of the box but it doesn't check types. So it builds extremely fast but I need to know my errors. Solution:

  webpackConfig.plugins!.push(
    new ForkTsCheckerWebpackPlugin({
      typescript: {
        configFile: cfg.paths.TS_CONFIG,
      },
    })
  );

How does it work? The build and the type-checking processes are run in parallel. The 2nd doesn't linger the 1st. You still can see the errors in the browser (and CLI) console. And visually too. Nice.

ES-Lint

For some reason, it stopped working. The solution was pretty simple:

  webpackConfig.plugins!.push(
    new ESLintPlugin({
      extensions: ['ts', 'tsx'],

Final results

  • File sizes
    • Before: 641 KiB + 234 KiB + 29.5 KiB (css) = 904.5 KiB
    • After: 221 KiB + 55 KiB + 8.1 KiB (o_O, css) = 284.1 KiB
    • After minifyIdentifiers: true (scary thing) & minifyWhitespace: true:  138 KiB + 44.2 KiB + 8.1 KiB = 190.1 KiB
  • DOMContentLoaded:
    • Before: 737ms
    • After: 231ms
    • After-2: 131ms
  • Build time: esbuild was ~3x faster

Afterthought

Taking a deeper look I found:

  • The main issue wasn't in babel. The main issue was that I used inline-source-map for prod. I didn't want to do it. Just a mistake. And both bundle analyzers didn't notice that. Even when I manually looked at the file I barely found it (the horisontal scroll was too big). Fixing this issue dropped the file size 3x times even with babel.
  • esbuild has a better wrapper logic for a ton of tiny files than babel does. 
  • Whitespace minification is quite important. GZip doesn't eliminate the difference. 
  • Name mangling is a beast. But be careful, it can easily break your app in runtime. 

How to use babel-module-resolver with vsCode and ESLint

4 декабря 2018

This article is a small guide about how to configure an enviroment for babel-plugin-module-resolver. This plugin allows you use custom prefixes for import/export declarations in JS files. E.g. import A from 'com/A'  can be treated as import A from ../../components/A.

  • At first you should install the package npm i -D babel-plugin-module-resolver
  • Then add your map-config to your .babelrc file into the plugins section. E.g.:
plugins:
[
  // ...
  [
    "babel-plugin-module-resolver",
    {
        "alias": {
           "^com(.+)": "./src/com/\\1"
        }
    }
  ],
  // ...
]
  • Then install npm i -D eslint-import-resolver-babel-module package for ESLint. It allows ESLint to check your rewrited imports, It assumes that you already use eslint-plugin-import package. If you don't I recommend you to start using it. This package checks your import/export declarations, it's very convinient.
  • Then change in your .eslint file:
{
  "parser": "babel-eslint",
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module",
    "ecmaFeatures": { "jsx": true }
  },
  "settings": {
    "import/resolver": { "babel-module": {} }
  },
  "plugins": [
    // ...
    "import"
  ],
  // ...
}
  • You also need to install babel-eslint if you didn't do it yet. And move its declaration from parseOptions to the root of the .eslintrc.
  • babel-module part in .babelrc section is just {}, it's okay.
  • Install ESLint plugin in vsCode
  • Probably you'll need to write in your .vcode/settings.json something like next lines. They force vscode to change CWD (current directory) in the deamon of eslint plugin:
"eslint.workingDirectories": [
	{
		"directory": "./client",
		"changeProcessCWD": true
	}
],
  • If you use path-intellisense plugin, you should consider configurate your mapping in .vscode/settings.json too. 
  • If you have any troubles, something doesn't work properly or doesn't work at all: Use force, Luke. Set vscode setting "eslint.trace.server": "verbose" and debug, debug, debug. I've spent on it several hours :(

Написание тестов для React с использованием enzyme

29 апреля 2017

Сразу отмечу, что статья не претендует на истину. Это всего лишь мои заметки о том, как вообще можно написать и запустить простые тесты для React, т.к. тут оказалось много места где заблудиться\окопаться. Итак, начнём.

Babel

Раз React значит и JSX, а значит и babel. Но простой запуск mocha просто запускает nodejs, в котором никакого JSX нет (и наверное никогда не будет). Что делать? Поиск подсказывает, что нужно запускать mocha (далее речь идёт именно о mocha) с флагом --compilers js:babel-core/register. Что сие есть? Это флаг заставит mocha предварительно компилировать код используя babel-core/register, который грубо изнасилуетподменяет require() в nodejs, что позволяет ему предварительно изуродоскомпилировать кодовую базу в нормальный javascript

Import\Export и .babelrc

Ок, но как babel-ю подсказать, что использовать в качестве конфига? Точного внятного ответа пока дать не могу, но как минимум можно создать в корне json-файл с именем .babelrc, который будет содержать json-конфиг. Мне прокатило, но в более сложных ситуациях нужно будет искать вариант поделикатнее. Собственно, чтобы не дублировать конфиг, имеет смысл подключить его и в webpack.config.js. Сразу отмечу, что взять и распарсить .babelrc как json простым вызовом require() nodejs почему-то не смогла (видимо мешает отсутствие расширения .json), поэтому fs.readFileSync и JSON.parse вам в помощь. 

Полифилы, JSDom и прочая нецензурщина

nodeJS это nodeJS, а не браузер, а для ряда тестов потребуется имитация браузера. И похоже, что на данный момент изящных решений нет, т.к. кодовая база react-* грубо сверяется с глобальным наличием window & document. Мде. Помимо прочего всякие полифилы, вроде regenerator-а (ну или отключить транспайлинг async-await в генераторы ещё до этапа компиляции) нужны. 

Для решения вопроса с полифилами достаточно import 'babel-polyfill'. а вот с JSDom так просто не срастётся. Я воспользовался таким решением:

  • import 'jsdom-global/register';
  • принудительно зафиксировал jsdom на 8-й версии (а не 10-й)

Замечу, что жизненноважно подключать этот костыль с JSDom до подключения React, иначе груды неадекватных глупых ошибок без внятных описаний украсят вашу безмятежную жизнь. Суть в том, что в недрах разных react-либ window кешируется ещё на этапе подключения файлов. И в последствии какой-нибудь флаг вида canUseDom === false будет вам яростно мешаться.

Можно подключать эти либы в каждом тест-файле, но тогда вы столкнётесь с тем, что при запуске сразу пачки тестов, babel-polifil будет падать со словами "я уже подключён", jsdom тоже будет что-нибудь базлать. Разумная альтернатива вынести обе эти строки в некий setup.js, а потом его подключать флагом --require tests/setup.js.

Сами тесты

React поставляет в пакете react-dom ряд инструментов для тестирования. Но даже на официальной странице этих инструментов упоминается, что вам стоит воспользоваться некой либой под названием enzyme. Установка оказалась не совсем тривиальной, т.к. сильно зависит от версии вашего React-а. Очень внимательно прочитайте инструкции по установке. Заодно доставьте к нему что-нибудь вроде chai для удобства тестирования.

Отмечу, что enzyme предоставляет по сути 3 набора утилит для работы с вашими React компонентами. А именно shallow для быстрого и поверхностного рендеринга, render для работы уже с html, и mount для полноценной работы с DOM и life-cycle React-компонентов. В любых маломальски сложных ситуациях готовьтесь к танцам с бубнами. Например .simulate метод для вызова событий умеет только React-события, а повешанные через addEventListener уже нет. Готовьтесь к нефинормативным ошибкам, куда же без них. 

Примеры кода для тестов можно посмотреть здесь. По правде говоря я, на данный момент, не в восторге ни от самой либы, ни от её документации. 

.babelrc & webpack

29 апреля 2017

При попытке подключить mocha к redux-react проекту мне потребовалось вынести конфигурацию для babel в .babelrc файл. Дальше веселее, оказалось, что mocha категорически отказывается запускаться с опцией { modules: false }, а вот без неё пожалуйста. На этом приключения не закончились, оказалось, что cuid не хочет запускаться с ней, а вот без неё пожалуйста. Временно решил разделить конфиги для babel & mocha, перебив эту опцию в webpack-конфиге. Но не тут то было. Экспериментальным путём выяснил, что webpack чихал на указанный вами в файле конфиг, если видит .babelrc. Решительно не понимаю, на кой чёрт так было сделано. Непродолжительный гуглёж привёл меня к тому, что ему нужно насильно запрещать это опцией babelrc: false в query для babel-loader-а.

Эх, web свернул куда-то не туда. Он, не успевши спозтись, уже куда-то расползается. И это я всё ещё не подключил тесты. Там несмотря на следование мануалам у меня пока вообще ничего не заводится. А гляда на рабочие конфиги из github-а я натыкаюсь на такие адовые конструкции по 500+ строк кода, альтернативные настройки babel с какими-то тестовыми preset-ами и пр., что…

JSX и вветвления ― jsx-control-statements

16 февраля 2017

Одна из раздражающих меня в React вещей, это помесь XML и JS синтаксиса. И ладно бы в одном файле, но, блин, даже в одном блоке. Сочетания XML и конструкций вроде {arr.map(arr => { очень сильно бьют, как по читаемости, так и по глазам в целом. К счастью, есть возможность этого избежать, используя jsx-control-statements. Т.к. его внедрение оказалось делом не совсем тривиальным, я решил написать об этому небольшой очерк.

Для начала, что нам даёт этот плагин?

<If condition={anyCondition}>...</If>
// вместо
{anyCondition && ...}

А также

<For each="el" index="key" of={arr}>...</For>
// вместо
{arr.map((el, key) => ...}

Т.е. XML-like конструкции, похожие на соответствующие им из XSLT (только более компактные). Также там есть ещё <Choice/> для switch-ей.

Ok, а как оно работает? Оно городит новые DOM-элементы? Нет. Оно ещё на уровне babel-я трансформируется в те самые && и .map. Т.е. это синтаксический сахар для эстетов. Ok, как подключить? 

  1. npm i jsx-control-statements
  2. в настройки babel-я в plugins добавляем "jsx-control-statements"

Всё, работает. А как насчёт lint-инга? Оказалось, что тут тоже всё схвачено (для eslint), но нужно немного понастраивать:

  1. npm i eslint-plugin-jsx-control-statements
  2. в настройках eslint
    1. в plugins добавляем jsx-control-statements
    2. в extends добавляем jsx-control-statements

Теперь никаких неудобств. Проблем с подстветкой синтаксиса тоже никаких (если у вас вообще JSX нормально отображается, конечно).