До Webpack 5 многие Node.js API автоматически подменялись браузерными
аналогами. При использовании модулей вроде crypto,
path, stream, buffer или
process Webpack silently добавлял соответствующие полифилы
в итоговый bundle.
Такое поведение приводило к ряду проблем:
Webpack 5 изменил философию работы: теперь автоматические полифилы отключены по умолчанию.
При попытке использовать Node.js API в браузерной сборке появляется ошибка:
Module not found: Error: Can't resolve 'crypto'
или:
BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
Такое поведение делает архитектуру более прозрачной и предсказуемой.
Webpack 4 автоматически подключал аналоги для следующих Node.js модулей:
| Node.js модуль | Типичный браузерный полифил |
|---|---|
buffer |
buffer |
crypto |
crypto-browserify |
stream |
stream-browserify |
path |
path-browserify |
util |
util |
assert |
assert |
os |
os-browserify |
http |
stream-http |
https |
https-browserify |
zlib |
browserify-zlib |
В Webpack 5 эти зависимости больше не подключаются автоматически.
Автоматические полифилы нарушали важный принцип:
браузерный код не должен случайно зависеть от Node.js окружения.
Пример проблемы:
import crypto from 'crypto';
export function hash(data) {
return crypto
.createHash('sha256')
.update(data)
.digest('hex');
}
В Node.js такой код работает естественно.
В браузере Webpack 4 автоматически подтягивал огромный пакет
crypto-browserify, включая дополнительные зависимости:
streambufferinheritsrandombytesbn.jsВ результате:
Webpack 5 делает подобные зависимости явными.
После обновления часто выясняется, что Node.js API используются не напрямую, а через сторонние библиотеки.
Типичный пример:
import jwt from 'jsonwebtoken';
Пакет jsonwebtoken зависит от crypto,
buffer, stream и других Node.js модулей.
В браузерной сборке появляется каскад ошибок:
Can't resolve 'crypto'
Can't resolve 'stream'
Can't resolve 'buffer'
Это означает, что библиотека изначально проектировалась для Node.js.
Существует несколько стратегий.
Наиболее правильный подход — использовать браузерные аналоги.
Например, вместо:
import crypto from 'crypto';
используется Web Crypto API:
const encoder = new TextEncoder();
const data = encoder.encode('hello');
const hashBuffer = await crypto.subtle.digest(
'SHA-256',
data
);
Преимущества:
Если библиотека действительно необходима, полифил подключается вручную.
npm install buffer process util
или:
yarn add buffer process util
resolve.fallbackWebpack 5 предоставляет механизм fallback.
Пример:
const webpack = require('webpack');
module.exports = {
resolve: {
fallback: {
buffer: require.resolve('buffer/'),
util: require.resolve('util/'),
process: require.resolve('process/browser')
}
},
plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser'
})
]
};
fallbackWebpack анализирует импорт:
import { Buffer } from 'buffer';
или:
require('buffer');
После этого выполняется перенаправление:
buffer -> buffer/
То есть вместо Node.js builtin-модуля используется npm-пакет.
Иногда модуль вообще не нужен в браузере.
Тогда используется значение false.
Пример:
module.exports = {
resolve: {
fallback: {
fs: false,
path: false,
net: false,
tls: false
}
}
};
Это особенно важно для серверных библиотек, которые частично импортируются в frontend.
fsРассмотрим код:
import fs from 'fs';
export function readConfig() {
return fs.readFileSync('./config.json');
}
Такой код не может работать в браузере:
При указании:
fallback: {
fs: false
}
Webpack явно сообщает о невозможности использования API.
После отказа от автоматических полифилов особенно важным становится разделение runtime-сред.
import fs from 'fs';
export function loadData() {
if (typeof window === 'undefined') {
return fs.readFileSync('./file.txt');
}
}
Даже несмотря на условие, Webpack всё равно анализирует импорт.
// server.js
import fs from 'fs';
export function loadData() {
return fs.readFileSync('./file.txt');
}
// client.js
export function loadData() {
return fetch('/api/data');
}
Современные библиотеки используют разные entry-point для браузера и Node.js.
Пример package.json:
{
"exports": {
".": {
"browser": "./browser.js",
"node": "./node.js",
"default": "./browser.js"
}
}
}
Webpack умеет автоматически выбирать подходящую реализацию.
browser в
package.jsonБолее старый механизм:
{
"browser": {
"fs": false,
"./server.js": "./browser.js"
}
}
Такой подход всё ещё широко используется.
targetНастройка target влияет на обработку встроенных
модулей.
module.exports = {
target: 'web'
};
Webpack ожидает браузерное окружение.
module.exports = {
target: 'node'
};
В этом режиме:
require('fs') сохраняется;Крупные приложения часто используют две сборки:
module.exports = [
{
target: 'web',
entry: './src/client.js'
},
{
target: 'node',
entry: './src/server.js'
}
];
Это позволяет полностью разделить окружения.
Некоторые старые npm-пакеты предполагают наличие Node.js API даже в браузере.
Типичные признаки:
process.nextTick;Buffer;stream;crypto;После перехода на Webpack 5 подобные библиотеки начинают ломаться.
Часто лучше заменить зависимость, чем подключать цепочку полифилов.
Вместо:
crypto-browserify
используется:
window.crypto.subtle
Вместо:
stream-browserify
используются:
После удаления полифилов bundle часто уменьшается на сотни килобайт.
Особенно тяжёлыми являются:
| Полифил | Размер |
|---|---|
| crypto-browserify | очень большой |
| stream-browserify | большой |
| buffer | средний |
| path-browserify | маленький |
Для поиска скрытых Node.js зависимостей используется:
npm ls crypto
или:
npm ls stream
Также помогает анализ bundle:
webpack --json > stats.json
Пакет:
npm install webpack-bundle-analyzer --save-dev
Конфигурация:
const {
BundleAnalyzerPlugin
} = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
Позволяет быстро обнаружить:
Webpack 4 автоматически добавлял:
processBufferglobalWebpack 5 этого больше не делает.
processnpm install process
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser'
})
]
};
Buffernpm install buffer
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer']
})
]
};
node
в Webpack 5В Webpack 4 существовала секция:
node: {
fs: 'empty'
}
В Webpack 5 этот механизм считается устаревшим.
Современная замена:
resolve: {
fallback: {
fs: false
}
}
process is not definedТипичная проблема после миграции:
ReferenceError: process is not defined
Причина:
Решение:
new webpack.ProvidePlugin({
process: 'process/browser'
})
или отказ от несовместимой библиотеки.
Buffer is not definedАналогичная ситуация:
ReferenceError: Buffer is not defined
Решение:
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer']
})
Основные причины:
Разработчик явно видит зависимости.
Bundle перестаёт раздуваться скрытыми полифилами.
Node.js и browser runtime больше не смешиваются неявно.
Большинство старых задач уже решаются нативными браузерными средствами:
| Node.js API | Современная альтернатива |
|---|---|
crypto |
Web Crypto API |
stream |
Web Streams API |
http |
Fetch API |
url |
URL API |
querystring |
URLSearchParams |
Типичный порядок действий:
Хорошей практикой считается:
crypto без крайней необходимости;stream-browserify;buffer глобально без причины;Frontend ecosystem постепенно уходит от browserify-совместимого подхода.
Современные инструменты:
предпочитают:
Отказ от автоматических полифилов стал важным этапом этой эволюции.