Ahead-of-time compilation
Elysia generates an optimized function for each route to make your app faster. The compilation happens only once, when the first request is received for each route. This is called Just-in-time (JIT) compilation.
But instead, you can use Ahead-of-time (AOT) compilation to remove the said overhead while making the app fast by moving the compilation to the build step.
Export your app
Section titled “Export your app”To use the AOT plugin, export your Elysia instance.
Export the instance
Section titled “Export the instance”In src/index.ts, export your Elysia instance as app so the AOT plugin can find it.
import { Elysia, t } from 'elysia'
export const app = new Elysia() .get('/hello/:name', { params: t.Object({ name: t.String({ minLength: 2 }) }) }, ({ params }) => `Hello, ${params.name}!`) .listen(3000)Create a build script
Section titled “Create a build script”Create build.ts with the same entry path in entrypoints and aot().
The script will dry run your app to capture the route registration and generate a bundle in dist/.
import { aot } from 'elysia/plugin/aot/bun'
const result = await Bun.build({ entrypoints: ['src/index.ts'], outdir: 'dist', target: 'bun', plugins: [aot('src/index.ts', { target: 'bun' })]})
process.exit(0)Build and run
Section titled “Build and run”Run the build from your project root, then start the generated bundle:
bun run build.tsbun dist/index.jsMultiple apps
Section titled “Multiple apps”The AOT plugin compiles your main app: the app export, else the default export, else the only Elysia instance the entry exports.
Every other exported Elysia instance with a name and at least one route gets its own manifest of compiled routes, keyed by that name:
import { Elysia, t } from 'elysia'
export const api = new Elysia({ name: 'api', prefix: '/api' }) .post('/state', { body: t.Object({ theme: t.String() }) }, ({ body }) => body)
export const app = new Elysia({ name: 'app' }).use(api)
export default appBoth app.handle() and api.handle() serve from their own manifests.
Any other Elysia instance in the bundle that builds its own router must be named and exported from the entry. This is any instance you call .handle(), .listen(), or .compile() on, read .fetch from, or mount with .mount(sub.fetch) or .mount(sub.handle). Otherwise, the build still succeeds, but the bundle throws at runtime:
[elysia-aot] App "<name>" has no AOT manifest. Name and export it, .mount() ones tooPlugins composed only with .use() need neither. Neither does .mount() with a plain function, such as Better Auth’s auth.handler.
When the build compiles more than one app, the main app must be named too, and each name must be unique.
Keep app names static, the same at build and at runtime. Don’t derive a name from an environment variable.
Benefits of AOT
Section titled “Benefits of AOT”Moving compilation to the build step gives your app:
- Faster startup and first requests. Route handlers and schemas are compiled before the server runs.
- Lower peak memory usage. The server avoids the temporary allocations needed for runtime compilation.
- Less runtime code. AOT analyzes unused Elysia modules and can remove TypeBox entirely when all TypeBox schemas can be compiled.
- Faster default merging. AOT precomputes how schema defaults are merged, reducing work when filling in missing values.
Generated route and schema code can increase bundle size and retained memory at startup. See the Elysia 2.0 AOT overview for details.
Detect AOT build
Section titled “Detect AOT build”You can programmatically detect if the app is running in an AOT build. Use this to conditionally initialize external services, such as database connections or third-party APIs, that are not needed during the build.
import { inAotBuild } from 'elysia'
if (!inAotBuild()) { // Initialize external services here.}Bundler and Runtime
Section titled “Bundler and Runtime”AOT is a bundler plugin that generates code for your Elysia app. It is compatible with Bun, Vite, esbuild, Rspack, and unplugin.
import { aot } from 'elysia/plugin/aot/bun'
await Bun.build({ entrypoints: ['src/index.ts'], outdir: 'dist', target: 'bun', plugins: [aot('src/index.ts', { target: 'bun' })]})
process.exit(0)import { defineConfig } from 'vite'import { aot } from 'elysia/plugin/aot/vite'
export default defineConfig({ build: { ssr: 'src/index.ts' }, ssr: { noExternal: ['elysia'] }, plugins: [aot('src/index.ts', { target: 'bun' })]})import * as esbuild from 'esbuild'import { aot } from 'elysia/plugin/aot/esbuild'
await esbuild.build({ entryPoints: ['src/index.ts'], bundle: true, platform: 'node', format: 'esm', outfile: 'dist/index.js', plugins: [aot('src/index.ts', { target: 'bun' })]})import { aot } from 'elysia/plugin/aot/rspack'
export default { entry: './src/index.ts', target: 'node', plugins: [aot('src/index.ts', { target: 'bun' })]}import { createUnplugin } from 'unplugin'import { aotFactory } from 'elysia/plugin/aot/unplugin'
const aot = createUnplugin(aotFactory)
export default { plugins: [aot.rollup({ entry: 'src/index.ts', target: 'bun' })]}Target
Section titled “Target”For specific runtime optimization, set the AOT plugin’s target to the runtime that executes the bundle:
aot('src/index.ts', { target: 'bun' })aot('src/index.ts', { target: 'node' })aot('src/index.ts', { target: 'workerd' })For Cloudflare Workers, use target: 'workerd' and follow the Cloudflare Workers integration for the deployment setup.