Exporting

One of Kobweb's major additions on top of Compose HTML is the export process.

This feature elevates the framework from one that produces a single-page application to one that produces a whole, navigable site. The export process takes snapshots of every page, resulting in better SEO support and a quicker initial render.

A normal development workflow will have you using kobweb run to iterate on your site, and then when you're ready to publish it, you'll kobweb export a production version.

A concrete export example

Let's take a moment to walk through this process in more detail, in order to demystify it.

If you weren't using Kobweb and were just using Compose HTML directly, you'd be recommended to create an index.html file that looks like this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Site Title</title>
</head>
<body>
<div id="root"></div>
<script src="mysite.js"></script>
</body>
</html>
Note

For example, you can find this exact structure recommended in the official Getting Started instructions.

What this does is declare a root <div> element whose children will get populated dynamically at runtime. The mysite.js script at the end of the file contains all the logic needed to generate every single page of your website.

This is very powerful, but when you build a website with this approach, you run into two major issues:

  1. As your codebase grows larger, mysite.js gets bigger and bigger, meaning a larger download is required before the site gets rendered. The initial view will just be an empty page until the script runs, which is dependent on the script's size and the user's download speeds.
  2. Search engines have a harder time indexing your site, because they can't see the content until the JavaScript executes. Any web crawler that doesn't execute JavaScript will never see anything more than a blank page.

OK, so let's add Kobweb into the mix. Here, we build a very minimal page and export our site (using kobweb export) to see what happens.

@Page
@Composable
fun ExampleKobwebPage() {
    Text("This is a minimal example to demonstrate exporting.")
}

Exporting generates the following HTML under your .kobweb/site folder, which I've reproduced here with a bunch of styles elided:

<!doctype html>
<html lang="en">
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>My Site Title</title>
  <meta content="Powered by Kobweb" name="description">
  <link href="/favicon.ico" rel="icon">
  <meta content="width=device-width, initial-scale=1" name="viewport">
 </head>
 <body>
  <div id="root" style="...">
   <style>...</style>
   <div class="..." style="min-height: 100vh;">
    This is a minimal example to demonstrate exporting.
   </div>
  </div>
  <script src="/mysite.js"></script>
 </body>
</html>

As you can see, Kobweb has filled out a bunch of extra information, although the site script it still linked to at the bottom of the file. This is important since, as mentioned earlier in this section, it contains all information necessary not just to render this page but the whole site.

In other words, you can download just this page and then continue to navigate around the site without needing to download any more files.

In short, the export process will discover all @Page-annotated methods in your codebase and generate a snapshot of each one. You can think of each snapshot as an SEO-friendly starting point from which you can access the rest of your site.

Exporting requires a browser

In order for Kobweb exporting to be able to take a snapshot of your site, it needs to spin up a browser in headless mode. This browser is responsible for loading the simple Compose HTML version of an index.html page and running its JavaScript to fill out the page. The browser will then get queried for the final html which Kobweb saves to disk.

Kobweb delegates much of this task to Microsoft's excellent Playwright framework. Hopefully this will be invisible to almost all users, but for advanced cases, it can be useful to know the technology that's running under the hood.

For custom CI/CD setups, you will at the very least need to be aware that the Kobweb export process requires a browser. For users who would like more information about this, we share a concrete example in a guide much later.

Using a local installation

If you have a local installation of Chrome, Edge, Firefox, or Safari that you would prefer to use, thereby avoiding a download step when you export for the first time, you can do this one of two ways:

  1. In your build script, set the kobweb.app.export.browserPath property and ensure that the kobweb.app.export.browser type property matches. It defaults to Browser.Chromium.
  2. Set the environment variable KOBWEB_EXPORT_BROWSER_PATH to the path. If it is not a Chrome installation, you must also set KOBWEB_EXPORT_BROWSER_TYPE (allowed values: Chromium, Edge, Firefox, and WebKit).

We seldom expose environment variables as a way to configure Kobweb build properties, but here, it feels natural since baking a specific, local path into a project's build script doesn't really make sense. This can also play nice with CIs, where it is trivial to set an environment variable.

Static layout vs. Full stack sites

There are two flavors of Kobweb sites: static and full stack.

Static layout sites

A static site (or, more completely, a static layout site) is one where you export a bunch of frontend files (e.g. html, js, and public resources) into a single, organized folder that gets served directly by a static website hosting provider.

In other words, you don't write a single line of server code. The server is provided for you in this case and uses a fairly straightforward algorithm - it hosts all the content you upload to it as raw, static assets.

The name static does not refer to the behavior of your site but rather that of your hosting provider solution. If someone makes a request for a page, the same response bytes get served every time (even if that page is full of custom code that allows it to behave in very interactive ways).

Full stack sites

A full stack site is one where you write both the logic that runs on the frontend (i.e. on the user's machine) and the logic that runs on the backend (i.e. on a server somewhere). This custom server must at least serve requested files (exactly the same job that a static web hosting service does) plus it likely also defines endpoints providing custom functionality tailored to your site's needs.

For example, maybe you define an endpoint which, given a user ID and an authentication token, returns that user's profile information.

Choosing the right site layout for your project

When Kobweb was first written, it only provided the full stack solution, as being able to write your own server logic enabled a maximum amount of power and flexibility. The mental model for using Kobweb during this early time was simple and clear.

However, in practice, most projects don't need the power afforded by a full stack setup. A website can give users a very clean, dynamic experience simply by writing responsive frontend logic to make it look good, e.g. with animations and delightful user interactions.

Additionally, many "Feature as a Service" solutions have popped up over the years, which can provide a ton of convenient functionality that used to require a custom server. These days, you can easily integrate auth, database, and analytics solutions all without writing a single line of backend code.

The process for exporting a bunch of files in a way that can be consumed by a static web hosting provider tends to be much faster and cheaper than using a full stack solution. Therefore, you should prefer a static site layout unless you have a specific need for a full stack approach.

Some possible reasons to use a custom server (and, therefore, a full stack approach) are:

  • needing to communicate with other, private backend services in your company.
  • intercepting requests as an intermediary for some third-party service where you own a very sensitive API key that you don't want to leak (such as a service that delegates to ChatGPT).
  • acting as a hub to connect multiple clients together (such as a chat server).

If you aren't sure which category you fall into, then you should probably be creating a static layout site. It's much easier to migrate from a static layout site to a full stack site later than the other way around.

Exporting and running

Both site flavors, static and fullstack, require an export.

To export your site with a static layout, use the kobweb export --layout static command, while for full stack the command is kobweb export --layout fullstack.

Once exported, you can test your site by running it locally before uploading. You run a static site with kobweb run --env prod --layout static and a full stack site with kobweb run --env prod --layout fullstack.

AppGlobals.isExporting

Sometimes, you have behavior that should run when an actual user is navigating your site but not at export time. For example, maybe you offer logged-in users an authenticated experience, but you'll never have a logged-in user when exporting.

You can determine if your page is being rendered as part of an export by checking the AppGlobals.isExporting property. This gives you the opportunity to manipulate the exported HTML or avoid side effects associated with page loading.

@Composable
@Layout
fun AuthenticatedLayout(content: @Composable () -> Unit) {
    var loggedInUser by remember { mutableStateOf<User?>(null) }

    if (!AppGlobals.isExporting) {
        LaunchedEffect(Unit) {
            loggedInUser = checkForLoggedInUser() // <- A slow, expensive method
        }
    }

    if (loggedInUser == null) {
        LoggedOutScaffold { content() }
    } else {
        LoggedInScaffold(user) { content() }
    }
}

Dynamic routes and exporting

Dynamic routes are skipped over by the export process. After all, it's not possible to know all the possible values that could be passed into a dynamic route.

However, if you have a specific instance of a dynamic route that you'd like to export, you can configure your site's build script as follows:

kobweb {
  app {
    export {
      // Special instance of dynamic route "/users/{user}/posts/{post}"
      addExtraRoute("/users/default/posts/0", exportPath = "users/index.html")
    }
  }
}
site/build.gradle.kts

Deploying

A static site gets exported into .kobweb/site by default (you can configure this location in your .kobweb/conf.yaml file if you'd like). You can then upload the contents of that folder to the static web hosting provider of your choice.

Deploying a full stack site is a bit more complex, as different providers have wildly varying setups, and some users may even decide to run their own web server themselves. However, when you export your Kobweb site, scripts are generated for running your server, both for *nix platforms (.kobweb/server/start.sh) and the Windows platform (.kobweb/server/start.bat). If the provider you are using speaks Dockerfile, you can set ENTRYPOINT to either of these scripts (depending on the server's platform).

Going in more detail than this is outside the scope of this README. However, you can read my blog posts for a lot more information and some clear, concrete examples:

Generating export traces

The Kobweb export feature is built on top of Microsoft Playwright, a solution for making it easy to download and run browsers programmatically.

One of the features provided by Playwright is the ability to generate traces, which are essentially detailed reports you can use to understand what is happening as your site loads. Kobweb exposes this feature through the export block in your Kobweb application's build script.

Enabling traces is easy:

plugins {
  // ... other plugins ...
  alias(libs.plugins.kobweb.application)
}

kobweb {
  app {
    export {
      enableTraces()
    }
  }
}
site/build.gradle.kts

You can pass in parameters to configure the enableTraces method, but by default, it will generate trace files into your .kobweb/export-traces/ directory.

Once enabled, you can run kobweb export, then once exported, open any of the generated *.trace.zip files by navigating to them using your OS's file explorer and drag-and-dropping them into the Playwright Trace Viewer.

Tip

You can learn more about how to use the Trace Viewer using the official documentation.

It's not expected many users will need to debug their site exports, but it's a great tool to have (especially combined with server logging) to diagnose if one of your pages is taking longer to export than expected.

Performance optimizations

At a high level, a huge chunk of the export step is Webpack processing your project, and then most of the time following that is Kobweb snapshotting your pages.

Note

Webpack is a module bundler, which means it is responsible for collecting all JS and CSS files in your project and combining them. It provides a bunch of configurable functionality, such as dead-code elimination, minification, etc. Kotlin/JS uses it as the solution for generating your web output. (In the future, Kotlin/JS plans to allow projects to switch their bundler, but that isn't available at the time of writing this note.)

For smaller sites, the export process takes less than a few minutes, and there's not too much you can do to speed it up significantly at that point. But for larger sites, the following sections can help dramatically.

Configuring webpack to be faster

If your site grows over time, you may see your export times increase significantly. A majority of this time is spent in Webpack, so we'll tackle that first.

Note

The solution provided in this section comes from user Ayfri, author of Kore. He ran into the issue, profiled his export, experimented with different configurations, and reported his experiences to me. For his project, he dropped the time he was spending in Webpack from 10 minutes down to less than 1 minute.

Changing two settings -- concatenation behavior and the minifier -- can drastically reduce the time it takes to process your site.

Module concatenation

Kotlin/JS generates a number of large files containing micro-modules: basically, code that is wrapped and treated as separated and isolated from everything else. For production builds, Webpack by default tries to combine all these modules safely, as that allows for more thorough dead-code elimination later.

However, this can be a massive hit to your export times! In the end, not concatenating your modules may only increase your final site by a modest few kilobytes. Users are not likely to notice the difference, either, as JavaScript engines are highly optimized to work with module closures.

Minification

By default, Webpack uses Terser to handle minification. However, it is very slow with large input files, due to a lack of parallelism. And Kotlin/JS generates a bunch of large, monolothic files.

In contrast, SWC, a suite of developer tools, has the SWC Minimizer, which uses a multithreaded algorithm even within a single file.

Optimizing your project

If you want to use the SWC minimizer, the first thing you need to do is add a dependency on it in your build script dependencies block. That looks like this:

kotlin {
    sourceSets {
        jsMain.dependencies {
            implementation(libs.kobweb.core)
            /*...*/
            // SWC has a minifier that is much faster than the webpack default
            // See: webpack.config.d/optimization.js
            implementation(devNpm("@swc/core", "1.16.1"))
        }
    }
}
site/build.gradle.kts

Second, if you create a webpack.config.d directory in your site's root and add some JavaScript in there, then Webpack will pick it up and run it.

Tip

Using webpack.config.d is probably the easiest way to configure Webpack for values that the KGP plugin doesn't expose.

Here, we recommend creating a file that will get run, abort if not in production mode, and otherwise configure the changes we discussed above.

// noinspection JSUnresolvedReference,NpmUsedModulesInstalled

// Kotlin/JS emits the whole app as one module, which doesn't play well with webpack's production defaults.

;(function () {
    if (config.mode !== 'production') return;
    const TerserPlugin = require('terser-webpack-plugin');
    config.optimization = config.optimization || {};
    config.optimization.concatenateModules = false;
    config.optimization.minimizer = [
        new TerserPlugin({
            // SWC is natively multithreaded, even per-file, providing a speed boost.
            // It is added in build.gradle.kts as a devNpm dependency.
            minify: TerserPlugin.swcMinify,
        }),
    ];
})();
site/webpack.config.d/optimization.js

You can, of course, play with these settings yourself. If you don't care about how long your CI/CD takes to run, you can leave this out entirely, keep concatenateModules set to true, tweak settings further, etc.

Snapshotting concurrency

Kobweb aims to snapshot its pages in parallel. By default, it looks at your system's available CPU core count and uses half of that number to spawn a bunch of workers.

You can configure this value by setting the kobweb.app.export.numThreads property in your build script. Since you may want to configure this value differently based on the environment you are exporting in (e.g., such as a CI/CD pipeline), you can also set the KOBWEB_EXPORT_NUM_THREADS environment variable.

If you set the value to 1 it disables parallelism and runs your snapshots sequentially.

If you are changing this value via the environment variable, you can also use a handful of special-case string values: "max" meaning use all available cores, "high" meaning use 75% of them, and "half" is, I hope, self-explanatory at this point! For example, you can set KOBWEB_EXPORT_NUM_THREADS=high.

Caution

Just because you can crank up the number of threads doesn't mean you should. If a system has limited CPU processing power (such as many free CI/CD runner tiers), the extra threads may significantly increase the time it takes each page to snapshot itself. The more workers you have, the more browser instances get started up at the same time. "Half" seems to be a good balance for many environments.

Tip

Consider reviewing the Exporting using GitHub Workflows article which includes an example of configuring this value.

This site is open source.
Copyright © 2026 Varabyte. All rights reserved.