Поиск по метке: Webpack
Babel to EsBuild migration for Webpack
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 migrate Javascript webpack configuration to Typescript
It turned out that the newest versions (at least 5+) of webpack support Typescript out of the box. So the algorithm is next:
- Create tsconfig.json at the root level. Content:
{ "compilerOptions": { "module": "CommonJS", "target": "ES5", "esModuleInterop": true, "checkJs": false, "strict": true, } }
- Rename all *.js files to *.ts
- Type all of them:
- no more ugly require, use import.
- webpack package has typings out of the box.
- You may find these types useful: Configuration & RuleSetRule
- To enable devServer write this:
interface Configuration extends WebpackConfiguration { devServer?: WebpackDevServerConfiguration; }
- Some of the popular plugins have types too.
- Some of them don't have types at all:
- Create a *.d.ts file
- Put there something like this:
declare module 'postcss-assets' { export default function postcssAssets(opts: { basePath: string; relative: boolean; }): unknown; }
- Make sure your webpack.config.ts file is placed at the root level. I mean exactly at the same spot where node_modules is. Otherwise, you won't be able to build it. No compilerOptions helped me.
- Run webpack. It should work.
Webpack и SCSS импорты в купе с module-resolver
С приходоим ES7 в JS пришли import-ы и export-ы. Жить стало веселее, и... сложнее. Появилось много заморочек и вообще новых проблем. Одна из таких проблем выглядит так: `import some from ../../../../../some`. Знакомая ситуация?
Обычно её в случае webpack-а решают при помощи модуля module-resolver. Он позволяет в .babelrc (или где вы держите конфигурацию) указывать регулярные выражения для автозамены, а также добавляет поддержку root-записей. Скажем с ним можно указать `import actions from '@actions/header', где `@actions` будет алиасом к какому-нибудь пути в вашем проекте.
Дальше встаёт вопрос инструментов. А как на это должен реагировать редактор? Как он узнает, что теперь пути в import-ах устроены хитрее. Что делать с линтингом? К счастью, для eslint есть такой плагин: eslint-plugin-import. С редакторами ситуация может быть сложнее.
Хорошо, а что делать с SCSS\Sass? Нормальных рабочих решений с наскоку мне найти не удалось. Но как оказалось, всё можно решить относительно просто. В настройках webpack для sass-loader-а можно указать настройки. Они будут переданы как есть в пакет node-sass. А вот он, оказывается, достаточно гибкий. Если там указать метод importer, то можно навязать свою логику обработки @import-ов. Пример:
{
loader: 'sass-loader',
options:
{
sourceMap: true,
url: false,
importer: url => (
{
file: url
.replace(...)
.replace(...)
.replace(...),
}),
},
}
Либо даже выцепить ваши настройки прямо из настроек в .babelrc.
Embedded webpack application
Возникла необходимость внедрять готовый SPA в другой проект на страницу. Проект сразу писался под это дело, но всё равно возник ряд проблем, которые нужно было решать. Напишу про 2 из них сюда как шпаргалку.
webpack & dynamic import
Что если ваш проект использует динамический импорт? Например System.import. В этом случае webpack сделает для подгружаемых файлов отдельный chunk. И грузить он его будет исходя из publicPath в настройках webpack. Но ведь embedded решения могут быть внедрены по любому пути. Как быть? Всё просто, пишем в точке входа что-нибудь типа: __webpack_public_path__ = window.myPredefinedPath и дело в шляпе. Магия. До загрузки приложения нужно определить этот window.myPredefinedPath, webpack его увидит и будет использовать. Сразу отмечу, что надо писать как есть, не window.__webpack...=, а как-будто эта переменная уже определена в коде. Если на неё ругается eslint не беда, пишем: // eslint-disable-line и он более не ругается.
webpack css & url
Возникла проблема с тем, что в стилях указаны пути к шрифтам так, что webpack их найти не может (отдельная тема почему так). Да и не должен, него его это забота. Однако он пытается от-revolve-ить все пути, что находит в css. Лечится установкой url: false в cssLoader-е. После этой настройки пути отдаются как есть без каких-либо проверок.
Что не так с Webpack server-ом и React Hot Loader-ом
Как минимум:
- Актуальная версия RHL в какой-то глубокой бете. Некоторые инструменты, такие как onsen monaca, в итоге, прибиты ногами к конкретной версии (иначе оно попросту не работает). Даже готовые пресеты для webpack-а завязаны на конкретные версии. Обратной совместимостью там и не пахнет. Даже сообщений внятных о том, что изменилось в stack trace-ах не ждите.
- Webpack server довольно хитро устроен внутри, поэтому такую связку очень сложно дебажить. Попробуйте ради интереса убрать [HMR] и [WDS] сообщения. В настройках плагинов такой опции нет. Попытка вырезать их из кода руками заставит вас детально разобраться в работе этого сервера. Ну или забить. Я забил :)
- Если ошибка произошла до первого успешного render-а, то stack trace показывает погоду на Марсе. Настоящая ошибка теряется в дебрях обёрток для ошибок.
- Если в Chrome Developer Tools для webpack's sourceMap-ов есть какая-то кривоватая, но поддержка, то в Firefox инструментах там просто хаос. Про stack trace-ы вообще молчу. Там просто ссылка строку в bundle.js.
- React Hot Loader по понятным причинам не перегружает обновления методов, которые были за-bind-ны в contructor-е компонента сами на себя. В итоге приходится перегружаться почти по любому поводу.
- Любые не тривиальные случаи приводят к необходимости обновить страницу.
Сложилось стойкое впечатление, что 3-ая версия React Hot Loader-а всё ещё слишком сырая, чтобы ей можно было пользоваться за пределами задач вёрстки. Но я бы не стал даже в их пределах. Этот инструмент оборачивает всё своими proxy-методами, чем сильно портит жизнь.
Поддержка sourceMap-ов тоже пока оставляет желать лучшего. Слишком часто при debug-инге проваливаешься невесть куда, не можешь поставить breakpoint, а то и вовсе наблюдаешь какие-то аномалии. Поддержка stacktrace-ов тоже пока далека от идеала.
Всё больше склоняюсь к разработке с ссылками на настоящие ресурсы, благо поддержка es6 в браузерах очень радует. Но вот JSX, я полагаю, нам в нативном виде в них не видать никогда. Так что вопрос, "а может к чёрту его этот JSX?" для меня лично достаточно актуален :)