Related
I have a React/Node + SSR application, I am trying to create a production build, I have managed to do that but the problem is that the files that I have in the build are too large.
I use latest version of react + webpack 4.
Here is my webpack configuration:
clientConfig.js
const path = require('path');
const common = require('./webpack.common-config');
const clientConfig = {
...common,
mode: 'production',
name: 'client',
target: 'web',
devtool: false,
entry: {
client: [
'#babel/polyfill',
'./src/client.js'
],
},
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: 'all',
name: 'vendor',
test: module => /node_modules/.test(module.resource),
enforce: true
},
common: {
chunks: 'all',
name: 'client'
}
},
},
},
node: {
fs: 'empty',
net: 'empty',
tls: 'empty',
}
};
module.exports = clientConfig;
serverConfig.js
const nodeExternals = require('webpack-node-externals');
const path = require('path');
const common = require('./webpack.common-config');
const serverConfig = {
...common,
mode: 'production',
name: 'server',
target: 'node',
devtool: false,
externals: [nodeExternals()],
entry: {
server: ['#babel/polyfill', path.resolve(__dirname, 'src', 'server.js')],
},
optimization: {
splitChunks: {
chunks: 'all',
}
},
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
chunkFilename: "[id].chunk.js"
},
node: {
console: false,
global: false,
process: false,
Buffer: false,
__filename: false,
__dirname: false,
},
};
module.exports = serverConfig;
commonConfig.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const common = {
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
include: [path.resolve(__dirname, 'src')],
query: {
presets: [
['#babel/preset-env', {loose: true, modules: false}],
"#babel/preset-react"
],
plugins: [
"#babel/plugin-proposal-class-properties"
]
},
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader'
]
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/'
}
}
]
}
]
},
plugins: [
new OptimizeCSSAssetsPlugin(),
new MiniCssExtractPlugin({
filename: "styles.css",
})
],
optimization: {
minimize: true,
minimizer: [new TerserPlugin()]
},
};
module.exports = common;
And another file which basically merges the client and the server config.
I run npm run build after that I run webpack -p --mode=production --optimize-minimize && node ./build/server.js
I get the following warning:
WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
vendor.js (667 KiB)
WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
client (928 KiB)
vendor.js
styles.css
client.js
Any advice or idea for the above size warning would be great! Thank you!
I recommend that you try core-js instead of babel/pollyfill this can help you to reduce your bundle size.
Also, I recommend that you try dynamic importing with react-loadable which supports SSR. there are different strategies for splitting your code. you can read more here(most important one)
In case you are using CSS frameworks such as bootstrap you should only use parts that you need and avoid importing all of them. there is a tool for purging unused CSS named purgecss but use it with caution and you have to know what are you doing.
In case you are using libraries such as lodash or material-ui you should import your module specifically to avoid importing all the packages into your bundle.
exp:
import debounce from 'lodash/debounce'
npm dedup or yarn dedup can help to remove duplicate dependencies inside bundle.
You may be able to get some reduction through adjusting your babel configuration. For instance, specifying some options for the "preset-env", such as bugfixes, targets if you don't have support older browsers, and using corejs polyfills instead of babel/polyfill.
"#babel/preset-env", {
"targets": {
"browsers": [
"last 2 years"
]
},
"bugfixes": true,
"useBuiltIns": "entry", // experiment with "entry" vs. "usage"
"corejs": 3
...
Depending on your codebase, the babel-plugin-transform-runtime may help too. I had some projects where it made a substantial difference, but other where it hasn't.
"#babel/plugin-transform-runtime",
{
"corejs": 3,
"version": "7.9.2"
}
Additional options for Webpack include using the webpack-cdn-plugin. This can greatly reduce your vendor bundle size. Of course, users will still have to download those same libraries, but they will be cached and don't need to re-downloaded every time your bundle changes or updates.
A little additional savings can be had by specifying runtimeChunk: true, and optionally inlining that chunk in your index.html with
new HTMLWebpackPlugin({
inlineSource: 'runtime~.+[.]js',
...
and the html-webpack-inline-source-plugin or similar.
You can split files to lower size to remove warning using splitChunks:
optimization: {
splitChunks: {
chunks: 'all',
minSize: 10000,
maxSize: 250000,
}
}
Check more for minSize and maxSize.
If you want to disable the warning, you can do it using performance
performance: {
hints: false
}
or remove warning for certain size limit like 1 MB:
performance: {
maxAssetSize: 1000000
}
I managed to switch to Next.js so all of this is not necessary anymore.
History:
I recently discovered an odd behaviour with using Webpack and dynamic imports. First I thought it might be the 3rd-party library 'Loadable Components' I used, so I opened a bug issue (https://github.com/gregberge/loadable-components/issues/517) on their end. The author replied telling me that the behaviour is coming from Webpack and the dynamic imports themselves.
I can stand the fact that it does not tree-shake the dynamic import, for me it is more important to understand why that is the case.
Demo repository to demonstrate the behaviour can be found here: https://github.com/dazlious/treeshaking-dynamic-imports
Short description of the problem: From my perspective, an imported named export should not force all the exported code to be bundled within it.
In the demo case we have a component (./lib/index.jsx) that has two sub components called module1 (./lib/module1/module1.jsx) and module2 (./lib/module1/module2.jsx). Module1 exports a constant called FOO_BAR that is then imported by Module2 as a named import.
When looking at the build output, you'll find Module2 containing Module1 in whole and not only the string that is specifically imported.
Is anyone with deep knowledge of Webpack and/or dynamic imports around here? Would be happy to learn more about the behaviour.
I edited the webpack.config to be:
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const baseDir = path.resolve(__dirname);
const config = {
mode: process.env.NODE_ENV,
stats: 'minimal',
resolve: {
extensions: ['.js', '.jsx'],
symlinks: false,
},
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'analyze.html',
}),
],
target: 'web',
devtool: 'hidden-source-map',
entry: {
bundle: [path.resolve(baseDir, 'lib')],
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
}),
],
mangleWasmImports: true,
splitChunks: {
cacheGroups: {
default: false,
vendors: false,
vendor: {
name: 'vendor',
chunks: 'all',
test: /node_modules/,
priority: 20
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 10,
reuseExistingChunk: true,
enforce: true
}
}
},
},
output: {
chunkFilename: '[name].[chunkhash].js',
publicPath: '/',
path: path.join(baseDir, 'dist'),
filename: '[name].[hash].js',
},
module: {
rules: [
{
test: /^.*\.jsx?$/,
include: [path.resolve(baseDir, 'lib')],
loader: 'babel-loader?cacheDirectory',
},
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
],
},
};
module.exports = config;
I think this has the result you are looking for?
image of bunde analyzer showing modules in their own bundles
I think it requires the splitChunks option to actually tree-shake the components properly.
I have spend a lot of time trying to figure webpack out, but I'm still guessing here.
const path = require("path");
const webpackMerge = require("webpack-merge");
const autoprefixer = require("autoprefixer");
const webpackCommon = require("./common.config");
// webpack plugins
const HtmlWebpackPlugin = require("html-webpack-plugin");
const DefinePlugin = require("webpack/lib/DefinePlugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const LoaderOptionsPlugin = require("webpack/lib/LoaderOptionsPlugin");
module.exports = webpackMerge(webpackCommon, {
bail: true,
devtool: "source-map",
mode: "production",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "[name]-[hash].min.js",
sourceMapFilename: "[name]-[hash].map",
chunkFilename: "[id]-[chunkhash].js",
publicPath: "/"
},
module: {
rules: [
{
test: /\.s?css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: [
{
loader: "css-loader",
options: {
sourceMap: true,
importLoaders: 2
}
},
{
loader: "postcss-loader",
options: {
config: {
path: path.resolve(__dirname, "postcss.config.js")
},
sourceMap: true
}
},
{
loader: "sass-loader",
options: {
outputStyle: "expanded",
sourceMap: true,
sourceMapContents: true
}
}
]
})
}
]
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, "../static/index.html"),
favicon: path.resolve(__dirname, "../static/favicon.ico"),
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
}
}),
new CopyWebpackPlugin([{ from: path.resolve(__dirname, "../static") }], {
ignore: ["index.html", "favicon.ico"]
}),
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '..'),
exclude: '.gitignore'
}),
new DefinePlugin({
"process.env": {
NODE_ENV: '"production"'
}
}),
new ExtractTextPlugin("[name]-[chunkhash].min.css"),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
ie8: true,
warnings: false
},
mangle: {
ie8: true
},
output: {
comments: false,
ie8: true
}
},
sourceMap: true
}),
new LoaderOptionsPlugin({
options: {
context: "/",
sassLoader: {
includePaths: [path.resolve(__dirname, "../src")]
}
}
})
]
});
Can't deploy my app to Heroku. Error: clean-webpack-plugin only accepts an options object. Any idea that will help me to solve this error? I checked the clean-webpack-plugin documentation updated some parts but still same error.
Take a look at the clean-webpack-plugin optional configurations. Your CleanWebpackPlugin is throwing this error:
constructor(options: Options = {}) {
if (isPlainObject(options) === false) {
throw new Error(`clean-webpack-plugin
// only accepts an options object. See:
// https://github.com/johnagan/clean-
// webpack-plugin#options-and-defaults-
// optional`);
}
}
Have you tried adding the CleanWebpackPlugin options to a webpack.config file? Something like this:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpackConfig = {
plugins: [
/**
* All files inside webpack's output.path
directory will be removed once, but the
* directory itself will not be. If using
webpack 4+'s default configuration,
* everything under <PROJECT_DIR>/dist/
will be removed.
* Use cleanOnceBeforeBuildPatterns to override
this behavior.
*
* During rebuilds, all webpack assets that are
not used anymore
* will be removed automatically.
*
* See `Options and Defaults` for information
*/
new CleanWebpackPlugin(),
],
};
module.exports = webpackConfig;
If all else fails, take a look at the complete Options/Defaults for the CleanWebpackPlugin. I think you can remove the [dist] and simply pass in the configuration object: https://github.com/johnagan/clean-webpack-plugin#options-and-defaults-optional
I think this bit of your code might be problematic. Try without the ['dist']:
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '..'),
exclude: '.gitignore'
}),
I have not worked with webpack merge so I would add that you should make sure you have the right dependencies in place, installed, and whether there are any conflicts.
Something to note: with zero configuration, clean-webpack-plugin will remove files inside the path directory you specified.
Using Webpack 4, I'm creating a bundle and a vendors bundle. Vendor bundle contains jQuery and the code bundle references jquery using 'import * from jquery'. This works perfectly.
However, I now need to use a 3rd party (already minified) javascript file. That file expects jquery to be exposed as '$' on the global window object. My bundle needs to be loaded after the new minified file, also.
So I have:
<script src='./libs/jquery-3.2.1.min.js'></script>
<script src='../vendor.js'></script>
<script src="./libs/newMinifiedFile.js"></script>
<script src="../bundle.js"></script>
as my current workaround. The static jquery file links the $ objects to the global namespace, and then the vendor.js file allows me to keep using 'import' in my bundle.
So, how do I only load jquery once, and use it in both ways? This seems to be a slightly different problem than most I've seen online because of how I'm loading things.
Here's a small example of my configs right now:
const config = (isDebug) => {
const isDevBuild = isDebug;
const extractCSS = new MiniCssExtractPlugin({filename: 'vendor.css'});
const sharedConfig = {
mode: isDevBuild ? 'development' : 'production',
stats: { modules: false },
resolve: {
extensions: [ '.js' ]
},
module: {
rules: [
{ test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, use: 'url-loader?limit=100000' },
]
},
entry: {
vendor: [
'jquery'
],
},
output: {
publicPath: 'dist/',
filename: '[name].js',
library: '[name]_[hash]',
},
plugins: [
new webpack.NormalModuleReplacementPlugin(/\/iconv-loader$/, require.resolve('node-noop')) // Workaround for https://github.com/andris9/encoding/issues/16
]
};
const clientBundleConfig = merge(sharedConfig, {
output: {
path: path.join(__dirname, 'wwwroot', 'dist'),
pathinfo: false
},
module: {
rules: [
{
test: /\.css(\?|$)/, include: path.resolve(__dirname, "client"), exclude: [/webviewer/, /redux/, /helpers/],
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
minimize: isDevBuild,
sourceMap: isDevBuild
}
}
]
}
]
},
plugins: [
extractCSS,
new webpack.DllPlugin({
path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'),
name: '[name]_[hash]'
})
],
optimization: {
minimize: !isDevBuild,
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
});
And in my normal config I use:
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require(path.join(__dirname, 'wwwroot', 'dist', 'vendor-manifest.json'))
})
So, I can find jquery in the global 'vendor_hash' object, but I can't get it to map it to $ no matter what I try (Most plugins seem to see that $ is never used in any of the code I'm supplying for the bundle and thus don't seem to include it). If I add something like this in my 'entry.js' file then it still doesn't work:
window.$ = window.jQuery = require("jquery");
I have a problem at import json dynamically using webpack.
(https://webpack.js.org/api/module-methods/#import-)
It keep shows this error - TS2307: Cannot find module
package versions are..
webpack version: 2.7.0
typescript version: 2.4.2
awesome-typescript-loader: 3.2.1
tsconfig.json
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "esnext",
"moduleResolution": "node",
"target": "es6",
"jsx": "react",
"allowJs": true,
"allowSyntheticDefaultImports": true
},
"awesomeTypescriptLoaderOptions": {
"useBabel": true,
"useCache": true
},
"exclude": ["node_modules"],
"include": [
"./components/**/*",
"./core/*",
"./pages/*",
"./utils/*",
"./urls/*",
"./translations/**/*",
"./main.tsx"
]
}
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const AssetsPlugin = require('assets-webpack-plugin');
const pkg = require('./package.json');
const isDebug = global.DEBUG === false ? false : !process.argv.includes('--release');
const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v');
const useHMR = !!global.HMR; // Hot Module Replacement (HMR)
const babelConfig = Object.assign({}, pkg.babel, {
babelrc: false,
cacheDirectory: useHMR,
});
// Webpack configuration (main.js => public/dist/main.{hash}.js)
// http://webpack.github.io/docs/configuration.html
const config = {
// The base directory for resolving the entry option
context: __dirname,
// The entry point for the bundle
entry: [
// require('babel-polyfill'),
'./main.tsx'
],
// Options affecting the output of the compilation
output: {
path: path.resolve(__dirname, './public/dist'),
publicPath: '/dist/',
filename: isDebug ? '[name].js?[hash]' : '[name].[hash].js',
chunkFilename: isDebug ? '[id].js?[chunkhash]' : '[id].[chunkhash].js',
sourcePrefix: ' ',
},
// Developer tool to enhance debugging, source maps
// http://webpack.github.io/docs/configuration.html#devtool
devtool: isDebug ? 'inline-source-map' : false,
// What information should be printed to the console
stats: {
colors: true,
reasons: isDebug,
hash: isVerbose,
version: isVerbose,
timings: true,
chunks: isVerbose,
chunkModules: isVerbose,
cached: isVerbose,
cachedAssets: isVerbose,
},
// The list of plugins for Webpack compiler
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
__DEV__: isDebug,
}),
// Emit a JSON file with assets paths
// https://github.com/sporto/assets-webpack-plugin#options
new AssetsPlugin({
path: path.resolve(__dirname, './public/dist'),
filename: 'assets.json',
prettyPrint: true,
}),
],
// Options affecting the normal modules
module: {
rules: [
{
test: /\.tsx?$/,
include: [
path.resolve(__dirname, './components'),
path.resolve(__dirname, './core'),
path.resolve(__dirname, './pages'),
path.resolve(__dirname, './apis'),
path.resolve(__dirname, './urls'),
path.resolve(__dirname, './translations'),
path.resolve(__dirname, './main')
],
loader: 'awesome-typescript-loader'
},
{
test: /\.css/,
use: [{loader: 'style-loader'}, {loader: 'css-loader', options:
{
sourceMap: isDebug,
// CSS Modules https://github.com/css-modules/css-modules
modules: true,
localIdentName: isDebug ? '[name]_[local]_[hash:base64:3]' : '[hash:base64:4]',
// CSS Nano http://cssnano.co/options/
minimize: !isDebug,
}},
// {loader: 'postcss-loader'}
],
},
{
test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/,
loader: 'url-loader?limit=10000',
},
{
test: /\.(eot|ttf|wav|mp3)$/,
loader: 'file-loader',
},
],
},
externals: {
react: 'React'
},
resolve: {
extensions: ['.js', '.ts', '.tsx']
}
};
// Optimize the bundle in release (production) mode
if (!isDebug) {
config.plugins.push(new webpack.optimize.UglifyJsPlugin({ compress: { warnings: isVerbose }, sourceMap: isDebug }));
config.plugins.push(new webpack.optimize.AggressiveMergingPlugin());
}
// Hot Module Replacement (HMR) + React Hot Reload
if (isDebug && useHMR) {
babelConfig.plugins.unshift('react-hot-loader/babel');
config.entry.unshift('react-hot-loader/patch', 'webpack-hot-middleware/client');
config.plugins.push(new webpack.HotModuleReplacementPlugin());
config.plugins.push(new webpack.NoEmitOnErrorsPlugin());
}
module.exports = config;
custom.d.ts
declare module "*.json" {
const value: any;
export default value;
}
my error code
// TS2307: Cannot find module './test.json'.
import('./test.json').then(_ => {
console.log(_
})
// but these codes work
import './test.json'
import('./test.js').then(_ => { console.log(_)})
I have searched issues as much as possible I can. but no one seems to encountered this issue.
Here's related links what I found.
https://github.com/Microsoft/TypeScript/issues/16820
https://blog.josequinto.com/2017/06/29/dynamic-import-expressions-and-webpack-code-splitting-integration-with-typescript-2-4/
If anyone have a clue, please let me know.
Are you sure your custom.d.ts is in a folder included in the compilation? I believe only typings in packages under node_modules/#types are included if they are not in the "files" or "include" properties in tsconfig.json.