Setup Preact with Webpack and Babel

Heather Koo
8 min readJan 8, 2022

--

Background

My team recently decided to rewrite the UI frontend of our web application in Preact from Python Flask. There were two main reasons:

  1. User experience
    The user experience became important as the application served more users with more features. We wanted a “Single Page Application(SPA)” where the users don’t have to wait for a new page to load.
  2. Developer experience
    When developing the frontend in Flask, we mixed HTML, Jinja2, and Javascript. JQuery was mostly used to perform ajax requests. As the repository grew bigger, it was hard to maintain and control the flow of multiple request.

So we decided to split the UI frontend from the Flask backend and picked Preact for frontend development.

Why Preact?

Among the many client-side frameworks, we picked Preact because it’s lightweight(4kB minified+gizipped) and fast. One disadvantage of SPA is it takes a long time to load large javascript code and this can lead slow performance. If you’re familiar with React, Preact will be a good alternative.

How to Set Up a Preact Project with Babel and Webpack

There is a tool “Preact CLI” to build a boilerplate code and it comes with its own configuration under the hood. While this is easy to start the app, the configuration is abstracted so it’s not easy to understand how things are configured when you need customization. I decided to set up our project from scratch using Babel and Webpack for full control. From my experience, I found it useful when I customize Ant Design(React web component library) themes and analyze/minimize javascript bundle size to improve the performance.

You can find the repository here. I used Ant Design for component library. If you don’t use it for your project, you can skip the Antd configuration part.

Webpack

Webpack is a Javascript module bundler that builds a dependency graph and combines every module your project needs into one or more bundles. This allows the client-side Javascript to import and export code across files like most programming languages. Bundlers like webpack find all the import statements and replace them with the actual contents of each required file. The final result is a single bundled Javascript file(by default, dist/main.js) with no import statements.

Here are some webpack related packages that need to be installed for our project.

  • webpack: It bundles Javascript files for usage in a browser.
  • webpack-cli: The official CLI of webpack that provides the interface of options webpack uses in its configuration file.
  • webpack-dev-server: It provides live reloading during development with “webpack serve” command
  • webpack-merge: It provides a “merge” function that produces a new object out of different objects. We use this package to merge common configuration with dev/prod configuration.
  • html-webpack-plugin: It simplifies creation of HTML files to serve your bundles. For example, this plugin adds <script defer src=“main.js”></script> in the index.html file for you.
  • webpack-bundle-anaylzer: It visualizes size of webpack output files with an interactive zoomable treemap. By default, you can view your bundle sizes on localhost:8888.
  • loaders — babel-loader, file-loader, style-loader, css-loader, less-loader, less

Let’s examine our webpack files. I separated development(webpack.dev.js) and production(webpack.prod.js) to build differently. In development, we want strong source mapping for debugging and a localhost server with live reloading or hot module replacement. In production, our goal shifts to focus on minified bundles, lighter weight source maps, and optimized assets to improve load time. For common configuration, we have webpack.common.js.

Webpack.common.js

publicPath

output: {
publicPath: "/",
},

We’re using HtmlWebpackPlugin which automatically adds script tag for bundled javascript file to the index.html. If you don’t set the publicPath, the script tag will look like this: <script src=“main.js”></script>. The browser tries to load this script by taking current domain+path and adding “main.js” onto the end. For example, if you navigate to http://lcoalhost:3000/a/b, the browser tries to load http://localhost:3000/a/main.js (Note that there’s no slash at the end of URL(b)). Without setting the publicPath, you’ll get a common error GET http://localhost:3000/a/main.js net::ERR_ABORTED 404 (Not Found).

Our main.js is hosted at http://localhost:3000/main.js. So the problem is the browser is trying to find the main.js at the incorrect path. How to solve this? We should set the publicPath as “/” so that the plugin creates the script tag like this: <script src=“/main.js”></script>. This means the browser will load up the script relative to just the current domain, not the current domain+path. In summary, the publicPath specifies the public URL address of the output files when referenced in a browser.

Aliasing

resolve: {
alias: {
react: “preact/compat”,
“react-dom”: “preact/compat”,
},
},

To alias any package in webpack, you need to add the resolve.alias section to your config. If you use the React ecosystem in your project (e.g. React web component library), you need to point all react and react-dom imports to Preact.

Loaders

In the module.rules, we use loaders to transform non-javascript types of files into valid modules and add it to the bundle. Out of the box, webpack only understands Javascript and JSON files. So we need loaders to bundle any type of module such as css, png files.

(Let’s skip babel-loader for now and start with file-loader.)

{
test: /\.(png|jpe?g|gif|woff|svg|eot|ttf)$/i,
use: [{ loader: “file-loader” }],
}

This tells the webpack’s compiler the following:

Hey, webpack compiler, when you come across a path that resolves to ‘png, jp(e)g, gif, woff, svg, eot, or ttf’ files inside of a require()/import statement, use the file-loader to transform it before you add it to the bundle.

We also use style-loader, css-loader, and less-loader to be able to import ‘scss, css, less’ files.

{
test: /\.scss|\.css|\.less$/,
use: [
“style-loader”,
“css-loader”,
{
loader: “less-loader”,
options: {
lessOptions: {
modifyVars: themeVariables,
javascriptEnabled: true,
},
},
},
],
}

The loaders are run from last to first, therefore our file will first go to less-loader which compiles less to css. I have specified an option here to customize theme in Antd. I imported the custom theme variables from a file “src/less/ant-theme-vars.less” instead of listing the variables in the webpack config file. Once the less files are resolved by less-loader and the option is applied, webpack uses css-loader to put css files into a string, and then style-loader adds these in the form of style tag in the head of your HTML file.

<style>
← your css →
</style>

Plugins

Plugins perform a wider range of tasks like bundle optimization, asset management and injection of environment variables.

plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
favicon: "./src/assets/images/favicon.png",
}),
new BundleAnalyzerPlugin({
analyzerHost: "0.0.0.0", // To make it work in the container
}),
],

html-webpack-plugin generates an HTML file for your application and automatically injects all your generated bundles into this file. You can set the index file path with the template option and favicon image file path with favicon option.

webpack-bundle-analyzer visualizes the size of webpack output files with an interactive zoomable treemap. By default, you can view your bundle sizes on localhost:8888. The option analyerHost: “0.0.0.0” is for docker container to bind to 0.0.0.0 instead of the default value 127.0.0.1. It will allow all connections from outside of the container to arrive.

webpack.dev.js

const { merge } = require("webpack-merge");
const commonConfig = require("./webpack.common");
const devConfig = {
mode: "development",
devtool: "inline-source-map",
devServer: {
port: 3000,
historyApiFallback: true,
proxy: {
"/api": {
target: "https://your-api-url",
changeOrigin: true,
},
},
},
};
module.exports = merge(commonConfig, devConfig);

I set mode to ‘development’ to specify it’s development configuration.

devtool: “inline-source-map” generates source maps to help debug the original code from the compiled code in the browser.

devServer is a configuration for webpack-dev-server which helps you automatically compile your code whenever it changes.

historyApiFallback: true enables routing within the Single Page Application without raising a 404 error. Single Page Applications (SPA) typically only utilise one index file that is accessible by web browsers: usually index.html. Navigation in the application is then commonly handled using JavaScript with the help of the HTML5 History API. This results in issues when the user hits the refresh button or is directly accessing a page other than the landing page, e.g. /help or /help/online as the web server bypasses the index file to locate the file at this location. As our application is a SPA, the web server will fail trying to retrieve the file and return a 404 — Not Found message to the user. So this configuration changes the requested location to the index you specify (default being /index.html). In other words, the dev servers delegate the route handling to the SPA. (Reference: https://burnedikt.com/webpack-dev-server-and-routing/).

proxy option is needed for dev to bypass CORS error when sending API requests. If you don’t enable CORS from the API server, you should proxy /api requests to the target URL. changeOrigin option has to be set as true to change the origin of the host header to the target URL for name-based virtual hosted sites.

webpack.prod.js

const prodConfig = {
mode: “production”,
};

I set mode to ‘production’ to specify it’s production configuration. It minifies the output by eliminating the dead code.

Babel

Babel is a Javascript transcompiler. It transpiles next generation JavaScript with features not yet available to all browsers (ES2015 and beyond) to older more compatible JavaScript (ES5). It also converts JSX syntax to js with presets.

Here is the list of babel related packages that need to be installed for our project.

  • @babel/core: Babel compiler core
  • @babel/plugin-transform-runtime: It deduplicates helper code across your compiled output.
  • @babel/preset-env: Babel preset for each environment. It defines which new Javascript features to transpile.
  • @babel/preset-react: Babel preset for all React plugins. It configures the transpiler for react.
  • babel-plugin-import: It loads the less-processed file but only for the components you actually use, supporting tree shaking. It reduces the bundle size of antd .less file.

I set up the babel configuration with the babel-loader in webpack.common.js.

{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-react", "@babel/preset-env"],
plugins: [
["@babel/plugin-transform-runtime"],
[
"@babel/plugin-transform-react-jsx",
{
pragma: "h",
pragmaFrag: "Fragment",
},
],
[
"import",
{
libraryName: "antd",
style: true,
},
],
],
},
},
},

Basically we’re telling webpack to look for any .js files (excluding ones in the node_modules folder) and apply babel transpilation using babel-loader with the @babel/preset-env, @babel/preset-react preset. In the plugins, @babel/plugin-transform-react-jsx to set up JSX in Preact syntax.

The import plugin enables modular import for Antd components, which reduces the bundle size significantly.

npm CLI scripts

Now that we walked through all the configuration. Let’s take a look at package.json scripts:

(Please ignore test and lint for now. I’ll explain them in the next article)

“scripts”: {
“start”: “webpack serve --config config/webpack.dev.js --open”,
“build”: “webpack --config config/webpack.prod.js”,
“test”: “jest --watch”,
“lint”: “eslint src tests --fix”
},

npm run start (or npm start) will run webpack serve command with the config file webpack.dev.js and the open option will open the browser after server had been started.

npm run build will run webpack command with the config file webpack.prod.js.

This is how I set up my own Preact boilerplate from scratch with Babel and Webpack. Initially, it was challenging to figure out the custom setup because there were not many resources. But I found Preact community very helpful and I really enjoyed working with Preact. I wanted to share my findings with others too and I hope this information is helpful for your project. Next time, I’ll post how I set up testing and linting for our project and containerize the app with docker. Please feel free to give me any feedback on the comment. Thank you!

--

--