The frontend-karaf-demo project is a maven project that builds a jar file containing a servlet and compiles a react application, with frontend-maven-plugin, into static files that can be added to the jar file’s classpath and be served by the servlet.
With the frontend-maven-plugin, building a webapp can be integrated into a maven build, without the builder having to download and install node.js. The frontend-maven-plugin downloads its own node.js to handle npm depdencies and do the build.
This bloggpost describes the things I had to do, to convert the webapp build in this setup from webpack to vite.
Note: to follow this recipe npm must be installed (but it is possible to achieve the same effects by manually editing package.json).
Remove all of the webpack stuff
Start by removing all of the webpack stuff
-
Remove the webpack config file
cd frontend-karaf-demo git rm src/main/frontend/webpack.config.js
In the frontend-karaf-demo project the webapp source resides in
src/main/frontend/and followed a typical webpack built react app structure -
Remove all of the webpack devDependencies from package.json (and remove other eslint and webpack stuff as well)
cd src/main/frontend/ npm uninstall --save-dev webpack webpack-cli eslint eslint-plugin-react eslint-webpack-plugin babel-loader @babel/core @babel/eslint-parser @babel/preset-env @babel/preset-react css-loader style-loader
(if there are other “-loader” devDependencies, add them to the list of devDependencies to uninstall)
Note: If you don’t have npm available on the command line, you can accomplish this step, by manually editing the “devDependencies” section of the package.json file to remove the above dependencies. The dependencies will still be in the package-lock.json file, but a new maven build of the project will update package-lock.json
-
Remove webpack, babel, and eslint config from the package.json file
modified src/main/frontend/package.json @@ -12,31 +12,5 @@ "redux": "^5.0.1", "redux-first-history": "^5.2.0", "redux-saga": "^1.3.0" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "parser": "@babel/eslint-parser", - "env": { - "browser": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "rules": { - "react/prop-types": "off" - } - }, - "babel": { - "presets": [ - "@babel/preset-env", - "@babel/preset-react" - ] } }
Add vite devDependencies
Remember to do this from the directory the package.json file residers in:
cd src/main/frontend/ npm i --save-dev vite @vitejs/plugin-react vite-plugin-simple-html @nabla/vite-plugin-eslint eslint-config-react-app @babel/plugin-proposal-private-property-in-object --legacy-peer-deps
Note: As when removing the webpack devdependencies, if you don’t have npm installed, this step can be accomplished by manually editing package.json to add the above dependencies in the “devDependencies” section of the file. Note that you will need to find the latest version of the packages manually. As when removing dependencies, a “mvn install” of the project will update the package-lock.json file.
Add a vite config file
Add a vite config file to the frontend directory. This replaces the webpack config file.
The file has the name src/main/frontend/vite.config.js, with the following contents:
import { defineConfig } from 'vite'; import eslintPlugin from "@nabla/vite-plugin-eslint"; export default defineConfig({ plugins: [eslintPlugin()], build: { minify: false, sourcemap: true, manifest: true, rollupOptions: { // overwrite default .html entry input: 'src/index.js', output: { entryFileNames: `assets/[name].js`, chunkFileNames: `assets/[name].js`, assetFileNames: `assets/[name].[ext]` } }, // Relative to the root outDir: '../../../target/classes', }, // Treat .js files as jsx esbuild: { include: /\.js$/, exclude: [], loader: 'jsx', }, });
Explanations:
- Import defineConfig and vite eslint support
- Add eslint as a vite plugin
-
The minify and sourcemap options do the same job as the devpack development mode, i.e.
module.exports = { mode: 'development',The vite options do the following 2 things:
- The “
minify: false” option tells vite not to minify the names of the react components, so that e.g. the react devtool will show the same components as the source code, instead of components named “xx” and “yy” - The “
sourcemap: true” option tells vite to create a sourcemap so that it is possible to find the correct source file and line names in stack traces in the devtools console
- The “
- Not sure what “
manifest: true” is for, but examples have it, so I kept it - The rollupOptions input settings tells vite to start in index.js instead of index.html (adding the index.html file to the jar file, is handled by maven outside of the frontend build)
- The rollupOptions output setting tells vite to use the fixed name asserts/index.js for the output, and not add a hash to the result filename
- The outDir setting tells vite to put the build result in a place where maven will include it as a resource in the jar file class path
- The esbuild setting tells vite to consider all .js files as jsx
Add eslint config file:
Add the file src/main/frontend/eslint.json, with the following contents:
{
"extends": "react-app"
}
Change the path and name of the js file in references to the file
If you, like I do, have the index.html file that bootstraps the webapp outside of the webpack/vite build, then replace
<script src="bundle.js" type="application/javascript"></script>
with
<script src="assets/index.js" type="application/javascript"></script>
The replaced frontend-maven-plugin and webpack setup, put the build results into the target/classes/ directory, which made them end up in the top directory level of the jar files.
The frontend-maven-plugin and vite setup puts the build results into the target/classes/assets/, which makes them end up in the assets subdirectory of the jar files.
Note! One place that may be affected by this location change, depending on your security setup, is providing anonymous access to the assets/index.js files (so that requests for the index.js file doesn’t get a 302 redirect to the login page).
I’m using Apache Shiro, which means I have had to do the following adjustment in my shiro.ini files:
modified handlelapp.web.security/src/main/resources/shiro.ini @@ -6,8 +6,7 @@ shiro.unauthorizedUrl = /unauthorized [users] [urls] -/bundle.js = anon -/bundle.js.map = anon +/assets/* = anon /open-iconic/** = anon /login = anon /api/** = anon
Another change is that vite puts the content of CSS files inside the build into a target/classes/assets/index.css file, instead of keeping it inside the index.js file, which means that, if your webapp source has CSS files, the index.html file needs to have this inside the <body> element of the HTML file:
<!-- Application's CSS --> <link href="/handlelapp/assets/index.css" rel="stylesheet">
One final place this change affected me, was that I used the location of the bundle.js to find the basePath to give to the react-router, to be able to handle a reverse proxy changing the app’s web context path.
The code I changed, looked like this (in the top level js file, the one that starts react and redux, and the router):
modified handlelapp.web.frontend/src/main/frontend/src/index.js @@ -18,7 +18,7 @@ import { SET_BASENAME, } from './reduxactions'; -const baseUrl = Array.from(document.scripts).map(s => s.src).filter(src => src.includes('bundle.js'))[0].replace('/bundle.js', ''); +const baseUrl = Array.from(document.scripts).map(s => s.src).filter(src => src.includes('assets/'))[0].replace(/\/assets\/.*/, ''); const basename = new URL(baseUrl).pathname; axios.defaults.baseURL = baseUrl; const sagaMiddleware = createSagaMiddleware();
Change webpack to “npm vite install” in frontend-maven-plugin config
To build with vite, replace the webpack section of the frontend-maven-plugin config with npx running the “vite build” command:
modified pom.xml @@ -179,10 +179,13 @@ </goals> </execution> <execution> - <id>webpack build</id> + <id>vite build</id> <goals> - <goal>webpack</goal> + <goal>npx</goal> </goals> + <configuration> + <arguments>vite build</arguments> + </configuration> </execution> </executions> </plugin>
After the change, the full frontend-maven-plugin config looks like this:
<project> <properties> <frontend.maven.plugin.version>1.15.0</frontend.maven.plugin.version> <nodejs.version>v20.12.0</nodejs.version> </properties> ... <build> <plugins> <plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <version>${frontend.maven.plugin.version}</version> <configuration> <nodeVersion>${nodejs.version}</nodeVersion> <workingDirectory>src/main/frontend</workingDirectory> <installDirectory>target</installDirectory> </configuration> <executions> <execution> <id>install node and npm</id> <goals> <goal>install-node-and-npm</goal> </goals> </execution> <execution> <id>npm install</id> <goals> <goal>npm</goal> </goals> </execution> <execution> <id>vite build</id> <goals> <goal>npx</goal> </goals> <configuration> <arguments>vite build</arguments> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
Configuration explanation:
- nodeVersion is the version of the node that frontend-maven-plugin will download use to do the build. This node is where frontend-maven-plugin finds its npm and npx executables
- workingDirectory is the directory path, relative to the pom.xml file, where the frontend code resides
- installDirectory is the directory where frontend-maven-plugin will install the node.js (in the target/node/ subdirectory)
The <executions> section is boilerplate and pretty much the same from project to project.
Comparing build times and result sizes
The speedup wasn’t nearly the 20-30 times speedup I’ve seen promised on the net.
Instead it looked like I was seeing a slowdown (on the first project I tried).
So I decided to convert all of my webapp frontend builds from webpack to vite and take some numbers.
I compared both dev builds (non-minified and with a source map) and production builds.
Then the numbers were less conclusive:
- Build times were about on par for webpack and vite in the development builds (vite marginally, but not significantly, slower than webpack on the average)
- Build times with vite were a lot shorter than webpack for production builds, webpack went from around 2s to 5s for this, and vite builds were a little, but consistently faster on production builds than on vite dev builds (as opposed to webpack production vs development build times)
- vite production builds were smaller than webpack production builds (an average of 6% size reduction compared to webpack)
- vite development builds were a lot smaller than webpack development builds (an average of 72% size reduction compared to webpack)
One reason for smaller vite build results is possibly that CSS used in the vite build is put into a separate .css file, instead of being packed into the bundle.
Loading CSS from a separate file, is a lot nicer for the web browser, and also a lot nicer for people trying to figure out why layout is happening by looking at the HTML and the CSS.
The builds were all run on the same laptop:
| Version | |
|---|---|
| Model | Acer Aspire 5 A515-45 |
| Memory | 16GB |
| CPU | AMD Ryzen 5, 6 cores, 3.6GHz |
| OS | debian 12.7 “bookworm” |
| Java | 21.0.4-ga |
| Maven | 3.8.7 |
| frontend-maven-plugin | 1.15.0 |
| node version | 20.12.0 |
| webpack | 5.91.0 |
| vite | 5.4.8 |
All builds were run 10 times and average build times values are the ones used in the tables.
Development builds
| Application | webpack build (s) | bundle.js size (bytes) | vite build (s) | index.js size (bytes) | time diff (s) | time diff % | size reduction (bytes) | size reduction % |
|---|---|---|---|---|---|---|---|---|
| frontend-karaf-demo | 1.33 | 2105379 | 1.59 | 572561 | 0.26 | 20 | 1532818 | 73 |
| sampleapp | 1.84 | 2124360 | 1.74 | 598136 | -0.1 | 5 | 1526224 | 72 |
| authservice | 2.13 | 2211038 | 1.70 | 523086 | -0.43 | 20 | 1687952 | 76 |
| ukelonn | 2.35 | 3301044 | 2.62 | 1011503 | 0.27 | 11 | 2289541 | 69 |
| handlereg | 2.20 | 2221697 | 2.14 | 621958 | -0.06 | 3 | 1599739 | 72 |
| oldalbum | 2.01 | 2610895 | 2.19 | 829698 | 0.18 | 9 | 1781197 | 68 |
| handlelapp | 1.62 | 2095126 | 1.67 | 597471 | 0.05 | 3 | 1497655 | 71 |
| rataoskr | 1.64 | 2095084 | 1.67 | 597431 | 0.03 | 2 | 1497653 | 71 |
| average | 0.03 | 9.12 | 1676597 | 72 |
Production builds
| Application | webpack build (s) | bundle.js size (bytes) | vite build (s) | index.js size (bytes) | time diff (s) | time diff % | size diff (bytes) | size diff % |
|---|---|---|---|---|---|---|---|---|
| frontend-karaf-demo | 4.48 | 311168 | 1.38 | 295287 | -4.02 | 70.00 | 49084 | 7 |
| sampleapp | 5.13 | 311364 | 1.47 | 307762 | -4.14 | 70.12 | 53234 | 7 |
| authservice | 5.27 | 280898 | 1.46 | 276680 | -4.20 | 70.02 | 59438 | 8 |
| ukelonn | 9.35 | 855629 | 2.34 | 521453 | -4.25 | 69.77 | 66341 | 9 |
| handlereg | 5.60 | 347984 | 1.92 | 324889 | -3.90 | 69.11 | 32861 | 5 |
| oldalbum | 6.15 | 429768 | 1.90 | 420702 | -3.93 | 69.50 | 34082 | 5 |
| handlelapp | 4.81 | 306176 | 1.49 | 307483 | -3.89 | 69.56 | 37209 | 5 |
| rataoskr | 4.83 | 306149 | 1.48 | 307474 | -3.96 | 69.64 | 41697 | 6 |
| average | -4.04 | 69.72 | 46743 | 6 |
Final words
In the webpack.config.js file that was removed there was the following setting:
module.exports = {
mode: 'development',
}
This setting caused the built bundle.js to not be minimized, which eased debugging.
In vite I have replicated this behavious with the minified and sourceMap settings:
export default defineConfig({ build: { minify: false, sourcemap: true, }, );
This makes the react devtool component tree look much nicer, and makes it possible to see the source code file of a stack trace entry.
As mentioned earlier in Comparing build times and result sizes, the speedup wasn’t nearly the 20-30 times speedup I’ve seen promised on the net.
Turns out that the promised speedup is when running a vite devserver with node.js.
And as a reader that has gotten this far already has established: I’m not running a devserver in node.js. I’m always building a file that is put on a jar file classpath and served by a server as a static file.
But I still will continue with vite, since this is the modern way to build and webpack has always felt a bit clunky, and also the resulting binaries are more compact than the ones from webpack.
One thought on “Convert react app built with frontend-maven-plugin from webpack to vite”