oveo is a javascript optimizer that works as a plugin for Vite, Rollup and Rolldown. It is written in Rust and uses oxc library for parsing and semantic analysis.
Use with caution!
Some optimizations are making assumptions that may break your code.
It is designed for bundlers that support hooks for transformations that work on individual modules and on the final chunk or a bundle.
- Add
@oveo/vitepackage as a dev dependency to a project. - Add oveo plugin to a Vite config:
import { defineConfig } from "vite";
import { oveo } from "@oveo/vite";
export default defineConfig({
plugins: [
// By default, all optimizations are disabled.
oveo({
hoist: true,
dedupe: true,
globals: {
include: ["js", "web"],
hoist: true,
singletons: true,
},
externs: {
import: [/* */],
},
renameProperties: {
pattern: "^[^_].+[^_]_$",
map: "property-map",
},
url: {
baseURL: "/assets/",
},
}),
]
});Vite setup with rolldown integration should use the
@oveo/rolldownplugin.
- Expression Hoisting
- Expression Deduplication
- Hoisting Globals
- Singletons
- Rename Properties
- Absolute URLs
This optimization works during module transformation phase and will try to hoist annotated expressions to the outermost valid scope.
To annotate an expression, it should be passed as an argument to the intrinsic function hoist(expr) or any function declared in the externs file.
By default, there is only one scope (program level scope). Scopes can be created with the intrinsic function 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 are describing a module @scope/modulename that has two functions with an additional behavior: myscope(() => {..}) and myfunc(any, hoistable_expr). The first argument in the myscope function will behave as an expression that creates a new hoist scope. The second argument in the myfunc function will be 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 the intrinsic function
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.
To prevent an expression from hoisting, it should be wrapped in ParenthesizedExpression, e.g.:
import { hoist } from "oveo";
const a = 1;
function test() {
hoist((() => a));
}This optimization works during chunk rendering phase and deduplicates expressions marked with the intrinsic function dedupe(expr) or when expression is hoisted.
- Deduped expressions shouldn't have any side effects.
- Deduped expressions doesn't provide referential equality (expressions from different chunks aren't deduplicated).
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_;This optimization works dunring chunk rendering phase and 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)) {
// ...
}
}This optimization works during chunk rendering phase and deduplicates objects like new TextEncoder() with an assumption that there are no mutations to this objects and this objects will be referential equal when they are referenced in the chunk file.
Currently, there are only two singleton objects: new TextEncoder() and new TextDecoder().
This optimization works during chunk transformation phase and renames property names that match a regexp pattern or properties from a property map.
When bundler finishes building all chunks, it will add new properties matching regexp pattern to a property map.
Property map has a simple key=value format:
left_=a
right_=b
status_=cPath to a property map file is specified in the oveo plugin options:
import { oveo } from "@oveo/vite";
export default {
input: "src/main.js",
output: {
file: "bundle.js",
},
plugins: [
oveo({
renameProperties: {
pattern: "^[^_].+[^_]_$",
map: "property-map",
},
}),
]
};Some minifiers support a similar optimization:
By default, when Rollup and Rolldown generates URLs to different assets, it generates relative URLs like this new URL("./asset", import.meta.url).href.
This optimization rewrites relative URLs into an absolute URL, e.g.:
function test() {
return new URL("./relative.css", import.meta.url).href;
}Will be transformed into:
function test() {
return "/base-url/relative.css";
}- Rollup supports
resolveFileUrlhook that can be used instead of this optimization. - Rolldown currently doesn't support
resolveFileUrlhook: issue#1010.
When optimizer is disabled, intrinsic functions will work as an identity function <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 are specified in the oveo plugin options:
import { oveo } from "@oveo/vite";
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 }]
}
}
}
}