Our CI pipeline was taking over 2 minutes for basic checks. After migrating to modern Rust-based tools, we cut that to under 15 seconds. Here’s the real data and how to do it yourself.
The Problem
Traditional JavaScript tooling is slow:
- ESLint: Written in JavaScript, single-threaded, plugins add overhead
- Jest: Heavy startup, complex configuration, slow transforms
- webpack: Complex bundling, slow rebuilds, memory-intensive
- tsc: Written in JavaScript, no parallelization
For a monorepo with 13+ packages, these tools were killing our developer experience.
Real CI Benchmarks
Data from optimex-pmm GitHub Actions:
Before Migration (Jan 16, 2026)
| Step | Duration |
|---|---|
| Lint (ESLint) | 100s |
| Typecheck (tsc) | 19s |
| Test (Jest) | 46s |
| Build (webpack) | ~30s |
| Total | ~195s |
After Migration (Jan 18, 2026)
| Step | Duration | Improvement |
|---|---|---|
| Lint (oxlint) | 1s | 100x faster |
| Typecheck (tsgo) | 2s | 9x faster |
| Test (Vitest) | 12s | 4x faster |
| Build (Rolldown) | 1s | 30x faster |
| Total | ~16s | 12x faster |
1. Replacing ESLint with Oxlint
Oxlint is a Rust-based linter that’s 50-100x faster than ESLint.
Installation
# Remove ESLint
yarn remove eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
# Add oxlint
yarn add -D oxlint
Configuration
Create oxlint.json:
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["import", "typescript"],
"rules": {
"import/no-duplicates": "error",
"@typescript-eslint/no-explicit-any": "error",
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "after-used",
"argsIgnorePattern": "^_",
"ignoreRestSiblings": true,
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_"
}
]
},
"ignorePatterns": ["**/dist/**", "**/node_modules/**", "**/*.spec.ts", "**/*.test.ts", "**/*.js"]
}
Scripts
{
"scripts": {
"lint": "oxlint --config oxlint.json apps libs --fix",
"lint:ci": "oxlint --config oxlint.json apps libs --deny-warnings"
}
}
Pre-commit Hook
Update lint-staged in package.json:
{
"lint-staged": {
"*.{js,ts,tsx}": ["oxlint --fix", "prettier --write"]
}
}
Result: Lint time dropped from 68s to <1s.
2. Replacing Jest with Vitest
Vitest is a Vite-native test runner with first-class TypeScript support.
Installation
# Remove Jest
yarn remove jest @types/jest ts-jest
# Add Vitest
yarn add -D vitest @vitest/coverage-v8
Configuration
Create vitest.config.ts:
import { resolve } from 'path'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['libs/**/*.{spec,test}.ts', 'apps/**/*.{spec,test}.ts'],
exclude: ['node_modules', 'dist'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
reportsDirectory: './coverage',
include: ['libs/**/src/**/*.ts', 'apps/**/src/**/*.ts'],
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/index.ts'],
},
passWithNoTests: true,
testTimeout: 30000,
hookTimeout: 30000,
setupFiles: ['reflect-metadata'], // For NestJS decorators
},
resolve: {
alias: {
// Add your monorepo aliases
'@myapp/shared': resolve(__dirname, 'libs/shared/src/index.ts'),
'@myapp/database': resolve(__dirname, 'libs/database/src/index.ts'),
},
},
})
Scripts
{
"scripts": {
"test": "vitest run",
"test:ci": "vitest run --coverage",
"test:watch": "vitest"
}
}
Key Benefits
- Native ESM: No babel/ts-jest transforms needed
- Watch mode: Instant re-runs on file changes
- v8 coverage: Built-in, fast coverage reporting
- Jest compatible: Most Jest APIs work out of the box
Result: Test time dropped from 46s to 12s.
3. Replacing webpack with Rolldown
Rolldown is a Rust-based bundler designed as a drop-in replacement for Rollup, with webpack-level features and blazing fast performance.
Why Rolldown?
- Written in Rust: Native performance, parallel processing
- Rollup-compatible: Same plugin ecosystem, familiar API
- Built for monorepos: Efficient handling of multiple packages
- Instant rebuilds: Sub-second builds even for large codebases
Installation
# Remove webpack
yarn remove webpack webpack-cli webpack-node-externals ts-loader
# Add rolldown
yarn add -D rolldown
Configuration
Create rolldown.config.mjs:
import { defineConfig } from 'rolldown'
export default defineConfig({
input: 'src/main.ts',
output: {
dir: '../../dist/apps/api-server',
format: 'cjs',
sourcemap: true,
entryFileNames: '[name].js',
},
// External: npm packages, scoped packages, node built-ins
external: [/^@/, /^[a-z]/, /node:/],
tsconfig: '../../tsconfig.json',
})
Key Configuration Points
- External dependencies: Use regex patterns to externalize all npm packages
- TypeScript support: Native TS support via
tsconfigoption - Output format: CommonJS for Node.js backends, ESM for libraries
Scripts
{
"scripts": {
"build": "rolldown -c"
}
}
Turbo Integration
For monorepos, configure Turbo to orchestrate builds:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["../../dist/**"],
"outputLogs": "errors-only"
}
}
}
webpack vs Rolldown Comparison
| Feature | webpack | Rolldown |
|---|---|---|
| Language | JavaScript | Rust |
| Cold start | 5-10s | <1s |
| Incremental | 2-5s | <100ms |
| Memory usage | High | Low |
| Config complexity | High | Simple |
Result: Build time dropped from ~30s to ~1s.
4. Faster Typechecking with tsgo
tsgo is Microsoft’s official native port of TypeScript, written in Go. It’s designed to be a drop-in replacement for tsc with significantly faster performance.
Installation
# Install the preview build
npm install @typescript/native-preview
# Use tsgo instead of tsc
npx tsgo --noEmit
VS Code Integration
A preview VS Code extension is available. Enable it in settings:
{
"typescript.experimental.useTsgo": true
}
Current Status
tsgo is still in development. Feature status:
- Complete: Parsing, type checking, JSX, build mode, incremental builds
- In Progress: JSDoc, declaration emit
- Prototype: Watch mode
- Not Ready: Language service API
Turbo Configuration
For monorepos, configure Turbo for caching:
{
"tasks": {
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
Scripts
{
"scripts": {
"typecheck": "tsgo --noEmit"
}
}
Result: Typecheck time dropped from 19s to 9s with tsc+cache, and to 2s with tsgo.
5. GitHub Actions CI
Complete workflow:
name: CI
on:
push:
branches: [main, staging, develop]
pull_request:
branches: [main, staging, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
node_modules
**/node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Lint
run: yarn lint:ci
- name: Typecheck
run: yarn typecheck
- name: Test
run: yarn test:ci
- name: Build
run: yarn build
Performance Summary
| Tool | Before | After | Speedup |
|---|---|---|---|
| Linting | ESLint (100s) | oxlint (1s) | 100x |
| Testing | Jest (46s) | Vitest (12s) | 4x |
| Building | webpack (30s) | Rolldown (1s) | 30x |
| Typecheck | tsc (19s) | tsc+cache (2s) | 9x |
| Total CI | ~195s | ~16s | 12x |
Migration Tips
- Migrate incrementally: Start with linting (biggest win, lowest risk)
- Keep ESLint rules parity: oxlint supports most common rules
- Test locally first: Run both old and new tools, compare output
- Update pre-commit hooks: Lint-staged with oxlint is blazing fast
- Use Turbo: Orchestration and caching multiply the benefits
- Rolldown for backends: Perfect for Node.js APIs with external dependencies
Migration Order
Recommended sequence for minimal risk:
- Oxlint - Drop-in replacement, immediate 100x speedup
- Vitest - Most Jest tests work unchanged
- Rolldown - Simple config for Node.js backends
- tsgo - Wait for stable release or use Turbo caching
Conclusion
Rust-based tools are production-ready and deliver massive performance improvements. The migration took about a day total and reduced our CI time by 12x. Developer experience improved dramatically with instant lint feedback, fast test runs, and sub-second builds.
The JavaScript ecosystem is being rewritten in Rust, and now is the time to adopt these tools.