IT — 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.
IT — 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.
IT — How to make a simple system.d service for a node.js server
Put this content:
[Unit]
Description={name}
[Service]
Type=simple
User={user}
ExecStart=/usr/bin/node {full-path-to-script}.js
[Install]
WantedBy=multi-user.target
... somewhere as {name}.service, where:
- {name} is the name of the service
- {user} is the name of the user to run the script (optional)
... then:
- run: this sudo ln -s /{full_path}/{name}.service /lib/systemd/system/{name}.service
- then this: sudo systemctl daemon-reload
- then this: sudo systemctl enable {name}.service
- and finally this: sudo systemctl start {name}
How does it work?
- It starts the service on boot (see WantedBy section).
- Using ExecStart command. Important: we specify the full path to node
- SystemD remembers the PID of the new process and considers the service is ongoing until the process is died.
- So any subsequent systemctrl start {name} won't do anything if the previous process is alive.
- This behavior is determined by Type=Simple
IT — Multilayout Keybinds in a Browser
Imagine that you want to add support of some keybinds in your web application. Let it be Ctrl+L for liking\unliking something. What kind of issues can you face in such a scenario?
CMD or Ctrl?
At first look at Ctrl. Probably on MacOS you'd like to replace it with CMD. You can check it by event.metaKey.
Extra modificators
Probably you wouldn't like to consider Ctrl+Alt+S as Ctrl+S. So don't forget to handle this case.
Different layouts
Not every language that uses the Latin alphabet has the L button at the same position as in a typical English keyboard layout. You need to decide what is more important to you ― a real key position on a keyboard or a letter upon of it. I'd guess that the 2nd case is preferable for most applications.
To get a real key position you can use which, code, codeKey properties. To get a letter on the key use key property.
Different alphabets
What's about Greek or Russian alphabets? Or any other possible alphabets? Or not even alphabets? There're different strategies. And one of them is to use a key from a typical English keyboard layout. So it leads us again to code and codeKey properties.
Example
const getEventKeyBind = event => {
const keybind = [];
if (event.metaKey) keybind.push('cmd');
if (event.ctrlKey) keybind.push('ctrl');
if (event.shiftKey) keybind.push('shift');
if (event.altKey) keybind.push('alt');
if (event.key === ' ') keybind.push('space');
else {
const key = event.key.toLowerCase();
if (key.length !== 1 || key.match(/^[a-z]$/)) {
// latin key or a special key
keybind.push(key);
} else {
// extra-latin or non-latin key
const [, enSymbol] = event.code.match(/^Key(\w)$/) || [];
keybind.push(enSymbol ? enSymbol.toLowerCase() : key);
}
}
return keybind.join('+');
};
IT — Custom English-German layout for Ubuntu
German language contains some extra latin symbols that English language doesn't: ä, ö, ü, ß. But if you choose German keyboard layout instead of English you get some keyboard keys moved to unusual positions. If it's okay for you then you don't need to do anything with it. Just get used to the new layout! But if you wanna stay with the English version of latin keys positions you need to find some convenient way to type German specific letters.
Under the cut 3 ways to handle it:
- Compose key
- International layout with "dead keys"
- Write your own EN-layout