oveo is a JavaScript optimizer that works as a plugin for Vite and Rolldown. It is written in Rust and uses the oxc library for parsing and semantic analysis.
It shrinks and speeds up production bundles by hoisting/deduplicating repeated expressions, hoisting global lookups (Array.isArray → cached local), deduplicating singletons (new TextEncoder()), shortening property names, and rewriting new URL(..., import.meta.url) asset references into absolute URLs.
Use with caution!
Some optimizations make assumptions that may break your code (see Caveats and safety and the assumptions listed under each optimization). All optimizations are disabled by default — enable them one at a time and validate your build output.
- When to use oveo
- Requirements
- Installation
- Quick setup
- Plugin options
- How it works
- Optimizations
- Annotating expressions
- Externs
- Caveats and safety
- Troubleshooting
- Development
- License
Use oveo when you ship a Vite/Rolldown production build and want smaller/faster output beyond minification:
- Repeated inline callbacks/objects/templates created inside components or hot functions.
- Hot paths calling
Array.isArray,Object.hasOwn,console.*,fetch, etc. new TextEncoder()/new TextDecoder()scattered across chunks.- Libraries like ivi that already emit hoist/dedupe annotations.
- Projects that can enforce a property-renaming convention (e.g. trailing
_) and assetbase.
Skip or be extra careful if you mutate globals (Array.isArray = ...), rely on new TextEncoder() !== new TextEncoder(), rely on object identity across chunks for deduped values, or cannot validate renamed properties end-to-end.
- Node.js
>= 20 - Vite (build mode only) or Rolldown
- Modules processed by the
transformstep:js,jsx,ts,tsx(configurable viafilter, see Plugin options)
npm install --save-dev @oveo/rolldown
# pnpm add -D @oveo/rolldown
# yarn add -D @oveo/rolldown
# bun add -d @oveo/rolldownIf your code (or a library like ivi) uses intrinsic calls such as hoist() / dedupe(), also install the identity fallback package (no-op at runtime, real behavior comes from the optimizer):
npm install oveoimport { defineConfig } from 'vite';
import { oveo } from '@oveo/rolldown';
export default defineConfig({
plugins: [
// By default, all optimizations are disabled.
oveo({
dedupe: true,
globals: true,
url: true, // auto-detect base from Vite `base`
}),
],
});Full example with every feature turned on:
import { defineConfig } from 'vite';
import { oveo } from '@oveo/rolldown';
export default defineConfig({
base: '/assets/',
plugins: [
oveo({
hoist: true,
dedupe: true,
// `globals: true` is a shorthand for everything enabled:
// {
// include: ['js', 'console', 'web', 'electron', 'tauri'],
// hoist: true,
// singletons: true,
// }
globals: true,
externs: {
import: ['./my-custom-extern.json'],
},
renameProperties: {
pattern: '^[^_].+[^_]_$',
map: 'property-map',
},
url: {
baseURL: '/assets/', // must end with '/'; or `url: true` to auto-detect from Vite `base`
},
}),
],
});import { oveo } from '@oveo/rolldown';
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
},
plugins: [
oveo({
dedupe: true,
globals: {
include: ['js', 'web'],
hoist: true,
singletons: true,
},
url: {
baseURL: '/assets/',
},
}),
],
};See examples/vite for a runnable Vite setup (vite.config.mjs, externs.json, properties.ini).
All fields are optional and default to disabled.
| Option | Type | Default | Description |
|---|---|---|---|
hoist |
boolean |
false |
Enable Expression Hoisting in transform. |
dedupe |
boolean |
false |
Enable Expression Deduplication in renderChunk. Hoisted expressions are deduped automatically. |
globals |
boolean | { include?, hoist?, singletons? } |
disabled | true = all namespaces + hoist + singletons. include is a subset of ['js', 'console', 'web', 'electron', 'tauri']. See Hoisting Globals and Singletons. |
externs.import |
string[] |
[] |
Paths/ids of extern files to load in buildStart (watched via addWatchFile). |
externs.inlineConstValues |
boolean |
false |
Inline {"type": "const", "value": ...} exports from extern files. |
renameProperties.pattern |
string |
unset | RegExp source for property names to rename. When set, new matches are appended to the map in writeBundle. See Rename Properties. |
renameProperties.map |
string |
unset | Path to key=value property map file. Loaded in buildStart, updated in writeBundle when pattern is set. Watched via addWatchFile. |
url |
boolean | { baseURL? } |
disabled | false/omitted = off. true or {} = auto-detect from Vite base. { baseURL } = explicit base (non-empty, must end with /). See Absolute URLs. |
filter |
Rolldown HookFilter |
{ moduleType: ['js', 'jsx', 'ts', 'tsx'] } |
Which modules go through transform. |
TypeScript shape (from @oveo/rolldown / @oveo/optimizer):
interface PluginOptions {
hoist?: boolean;
dedupe?: boolean;
globals?:
| boolean
| {
include?: Array<'js' | 'console' | 'web' | 'electron' | 'tauri'>;
hoist?: boolean;
singletons?: boolean;
};
externs?: {
inlineConstValues?: boolean;
import?: string[];
};
renameProperties?: {
pattern?: string;
map?: string;
};
url?: boolean | { baseURL?: string };
filter?: HookFilter;
}oveo is designed for bundlers with per-module and per-chunk hooks (transform and renderChunk).
| Optimization | Phase | Needs annotations? |
|---|---|---|
| Expression Hoisting | transform (module) |
Yes (hoist/scope or comments/externs) |
| Expression Deduplication | renderChunk (chunk) |
Yes (dedupe/@__CONST__), hoisted exprs included automatically |
| Hoisting Globals | renderChunk (chunk) |
No (namespace allowlist) |
| Singletons | renderChunk (chunk) |
No (TextEncoder/TextDecoder only) |
| Rename Properties | transform + writeBundle |
No (pattern + map) |
| Absolute URLs | renderChunk (chunk) |
No (new URL(..., import.meta.url) patterns) |
Sourcemaps are preserved (transform/renderChunk return { code, map }). Transform failures report Unable to transform module '<id>'; chunk failures report Unable to optimize chunk file.
- Expression Hoisting
- Expression Deduplication
- Hoisting Globals
- Singletons
- Rename Properties
- Absolute URLs
Works during module transformation. Tries to hoist annotated expressions to the outermost valid scope.
Annotate with comment /*@__HOIST__*/expr (preferred) or intrinsic hoist(expr) (see Annotating expressions):
function test() {
const x = /*@__HOIST__*/ (c) => a;
return x;
}Comment annotations are matched by substring (block or line comments in leading position, e.g. /* note @__HOIST__ */ also works). Annotation comments are removed from the output.
By default there is only one scope (program-level scope). Create scopes with /*@__SCOPE__*/(() => {..}) (or scope(() => {..})), or with a function declared in the externs file.
{
"@scope/modulename": {
"exports": {
"myscope": {
"arguments": [{ "scope": true }]
},
"myfunc": {
"arguments": [{}, { "hoist": true }]
}
}
}
}In this externs example we describe module @scope/modulename with two functions: myscope(() => {..}) and myfunc(any, hoistable_expr). The first argument of myscope behaves as an expression that creates a new hoist scope. The second argument of myfunc is hoisted to the outermost valid scope.
import { myscope, myfunc } from '@scope/modulename';
import { x } from './module.js';
const fn = myscope((inner_0) => {
myfunc(1, (inner_1) => {
x(inner_1);
myfunc(2, () => {
x(inner_0);
});
myfunc(3, (inner_3) => {
x(inner_3);
});
});
});Will be transformed into:
import { myscope, myfunc } from '@scope/modulename';
import { x } from './module.js';
const _HOIST_3 = (inner_3) => {
x(inner_3);
};
const fn = myscope((inner_0) => {
const _HOIST_2 = () => {
x(inner_0);
};
myfunc(1, (inner_1) => {
x(inner_1);
myfunc(2, _HOIST_2);
myfunc(3, _HOIST_3);
});
});import { component, getProps, html } from "ivi";
import { type Action, dispatch, select } from "./actions.js";
const Button = component((c) => {
return ({ text }) => html`
<button @click=${() => { dispatch(c, select(getProps(c).entry)); }}}>
${text}
</button>
`;
});In the example above, component(() => {}) behaves as a hoisting scope (declared in the externs file) and ivi template compiler annotates event handlers as hoistable expressions.
After template compilation and oveo optimizations the generated code will look like:
import { component, getProps, _T, _t } from "ivi";
import { type Action, dispatch, select } from "./actions.js";
const _TPL_ = _T(/* template strings and opcodes */);
const Button = component((c) => {
const _HOISTED_ = () => { dispatch(c, select(getProps(c).entry)); };
return ({ text }) => _t(_TPL_, [_HOISTED_, text]);
});Terminology:
- "Hoist Scope" - scope that can contain Hoisted Expressions. By default, there is only a program level scope. Additional scopes can be created with
/*@__SCOPE__*/. - "Hoisted Expression" - expression that should be hoisted to the outermost Hoist Scope.
- "Hoisted Expression Scope" - scopes created inside of a hoisted expression.
- "Inner Scope" - the closest Hoist Scope.
- "Outer Scopes" - scopes outside of the closest Hoist Scope.
// outer scope (hoist scope - root scope)
{
// outer scope
scope((a) => {
// inner scope (hoist scope)
return () => {
// inner scope function
if (a) {
// conditional prevents hoisting
hoist((i) => {
// hoisted expr
// hoisted expr scope
i(); // symbol from the hoisted expr scope
a(); // symbol from the inner scope
});
}
};
});
}Hoisting heuristics are quite conservative:
- All symbols should be accessible from the Hoist Scope.
- Hoisted expression should have a type:
ArrowFunctionExpression-() => {}FunctionExpression-function () {}CallExpression-fn()NewExpression-new C()ObjectExpression-{ key: value }ArrayExpression-[a, b, c]TemplateLiteral-`text ${sym}`TaggedTemplateExpression-tpl`text ${sym}`
- No conditionals on the path to the Hoist Scope:
ConditionalExpression-cond ? then : elseIfStatement-if (cond) { .. } else { .. }SwitchStatement-switch (v) { }
- Expressions hoisted to the Inner Scope should be inside of a function scope.
Works during chunk rendering. Deduplicates expressions marked with /*@__CONST__*/expr (or dedupe(expr)), or expressions already hoisted.
- Deduped expressions shouldn't have any side effects.
- Deduped expressions don't provide referential equality across chunks (dedup is chunk-local).
import { dedupe } from 'oveo';
import { externalIdentifier } from './module.js';
const obj1 = dedupe({
global: Number,
identifier: externalIdentifier,
array: [1, 2, 3],
literal: 1,
});
function Scope1() {
const obj2 = dedupe({
global: Number,
identifier: externalIdentifier,
array: [1, 2, 3],
literal: 1,
});
const scoped1 = dedupe({ array: [1, 2, 3] });
}
function Scope2() {
const scoped2 = dedupe({ array: [1, 2, 3] });
}
const arr1 = dedupe([1, 2, 3]);Will be transformed into:
import { externalIdentifier } from './module.js';
const _DEDUPE_ = [1, 2, 3];
const obj1 = {
global: Number,
identifier: externalIdentifier,
array: _DEDUPE_,
literal: 1,
};
function Scope1() {
const obj2 = obj1;
const scoped1 = { array: _DEDUPE_ };
}
function Scope2() {
const scoped2 = { array: _DEDUPE_ };
}
const arr1 = _DEDUPE_;Works during chunk rendering. Hoists global values and their static properties.
It hoists only predefined globals with an assumption that they aren't mutated.
function isArray(data) {
if (Array.isArray(data)) {
// ...
}
}
function from(data) {
if (Array.from(data)) {
// ...
}
}Will be transformed into:
const _GLOBAL_1 = Array;
const _GLOBAL_2 = _GLOBAL_1.isArray;
const _GLOBAL_3 = _GLOBAL_1.from;
function isArray(data) {
if (_GLOBAL_2(data)) {
// ...
}
}
function from(data) {
if (_GLOBAL_3(data)) {
// ...
}
}Configure with globals: true (all of ['js', 'console', 'web', 'electron', 'tauri'] + hoist + singletons) or granularly:
oveo({
globals: {
include: ['js', 'web'],
hoist: true,
singletons: true,
},
});Works during chunk rendering. Deduplicates objects like new TextEncoder() with an assumption that there are no mutations to these objects and that these objects are referentially equal when referenced in the chunk file.
Currently only two singletons: new TextEncoder() and new TextDecoder(). Enabled via globals: true or globals: { singletons: true }.
Works during chunk transformation. Renames property names matching a RegExp pattern or listed in a property map.
When bundler finishes building all chunks, it adds new properties matching the RegExp pattern to the property map.
Property map has a simple key=value format:
left_=a
right_=b
status_=cPath to the property map file is specified in the oveo plugin options:
import { oveo } from '@oveo/rolldown';
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
},
plugins: [
oveo({
renameProperties: {
pattern: '^[^_].+[^_]_$',
map: 'property-map',
},
}),
],
};Workflow:
- First build with
pattern+map: matching properties are renamed and the map file is created/updated inwriteBundle. - Commit the map file. Subsequent builds reuse stable short names.
- To rename-only (no new names), set
mapwithoutpattern.
To mark a single string literal as a property name, use key('prop_') (see Annotating expressions).
Some minifiers support a similar optimization:
Since oxc recently added support for property mangling, this optimization will be removed in the future versions.
By default, when Rollup and Rolldown generate URLs to different assets, they generate relative URLs like new URL("./asset", import.meta.url).href.
This optimization rewrites relative URLs into absolute URLs, e.g.:
function test() {
return new URL('./relative.css', import.meta.url).href;
}Will be transformed into:
function test() {
return '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/base-url/relative.css';
}- Rollup supports
resolveFileUrlhook that can be used instead of this optimization. - Rolldown currently doesn't support
resolveFileUrlhook: issue#1010.
Supported patterns (first argument must be a string literal or a substitution-free template literal, second argument must be import.meta.url):
new URL('./asset', import.meta.url).hrefnew URL('./asset', import.meta.url).pathnamenew URL('./asset', import.meta.url)["href"]/["pathname"]new URL('./asset', import.meta.url).toString()(zero arguments, also["toString"]())
Skipped (left untouched): absolute URLs (https:…, data:…), root-absolute (/…), query/hash-only (?…, #…), empty strings, and shadowed (non-global) URL constructors. baseURL must be non-empty and end with '/'. In plugin options, url: true auto-detects the base from Vite base.
| Goal | Comment (preferred) | Intrinsic (from oveo) |
|---|---|---|
| Hoist to outer hoisting scope | /*@__HOIST__*/expr |
hoist(expr) |
| Create hoisting scope | /*@__SCOPE__*/(() => {..}) |
scope(() => {..}) |
| Deduplicate | /*@__CONST__*/expr |
dedupe(expr) |
| Rename string as property | — | key(string_literal) |
When the optimizer is disabled, intrinsic functions work as identity functions <T>(expr: T) => expr.
Hoists expression to the outermost valid hoisting scope.
Creates a new hoisting scope.
Deduplicates expressions.
Renames string literal as a property name.
Extern files describe third-party modules so oveo can treat their functions as hoist/scope annotations, or inline constants — without changing that library's source. Paths are specified in plugin options:
import { oveo } from '@oveo/rolldown';
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
},
plugins: [
oveo({
externs: {
import: [
'ivi/oveo.json', // Distributed in the 'ivi' package
'./my-custom-extern.json',
],
},
}),
],
};Extern file example:
{
"@scope/modulename": {
"exports": {
"fnWithHoistableArg": {
"type": "function",
"arguments": [{ "hoist": true }]
},
"fnWithHoistScopeArg": {
"type": "function",
"arguments": [{ "scope": true }]
}
}
}
}Supported export descriptors:
{ "type": "function", "arguments": [{ "hoist"?: true, "scope"?: true }] }— per-argument hoist/scope behavior.{ "type": "const", "value": ... }— inline constant (requiresexterns: { inlineConstValues: true }). Example inexamples/vite/externs.json.
- Globals: assumes globals and their static properties are never reassigned/monkey-patched. Don't enable
globals.hoistif you (or a dependency) mutateArray,Object,console,window, etc. Limitincludeto namespaces you control (e.g.['js']). - Singletons: assumes
new TextEncoder()/new TextDecoder()instances are never mutated and can share identity within a chunk. - Dedupe: only for side-effect-free expressions; equality is chunk-local, not cross-chunk.
- Hoist: heuristic is conservative (see Hoisting Heuristics); symbols must be reachable from the hoist scope, no conditionals on the hoist path. Opt out with extra parens (
hoist((() => a))stays put). - Rename properties: irreversible across builds without the map file — commit
renameProperties.map, review new entries, and avoid overly broadpatternregexes. - URLs: only the patterns listed in Absolute URLs are rewritten;
base/baseURL(must end with/). Relative./Vitebasedisables auto-detection with a warning.
oveo: url.baseURL must end with '/'— append trailing slash or useurl: truewith a proper Vitebase(not'./'or'').oveo: ignoring unsupported Vite `base` ...— auto-detection disabled; seturl: { baseURL: '/...' }explicitly.Unable to read property map file— normal on first build whenpatternis set and the file doesn't exist yet; it will be created inwriteBundle. Withoutpattern, ensure the path exists.Invalid property map file/Unable to import extern file— checkkey=valueformat and that extern paths resolve (they go through Rolldownresolve+addWatchFile).- Nothing happens — remember all optimizations default to off; set at least
dedupe: trueorglobals: true. Also checkfiltercovers yourmoduleTypeand that Vite runs in build mode (apply: 'build'meansvite devis untouched).
bun install
bun run napi-build # napi build
bun run build # tsc -b
bun run test # bun test ./tests/
bun run format # oxfmt
bun run check # oxlintNAPI binding lives in packages/@oveo/optimizer (bun run napi-build). JS plugin lives in packages/@oveo/rolldown/src/index.ts. Intrinsics live in packages/oveo.
MIT — see LICENSE.