├── .gitignore ├── docs ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── ts-function.png │ └── results │ │ └── benchmark-results.json ├── tsconfig.json ├── package-lock.json ├── package.json ├── .gitignore ├── composables │ ├── useBenchmarkData.ts │ ├── utils │ │ ├── CommonUtil.ts │ │ └── ChartUtil.ts │ ├── updateBubbleChart.ts │ ├── updateBarChart.ts │ ├── renderBarChart.ts │ └── renderBubbleChart.ts ├── nuxt.config.ts ├── app.vue ├── components │ ├── Tooltip.vue │ └── Select.vue └── pages │ └── index.vue ├── projects └── thousand-functions │ ├── .gitignore │ └── rslib.tsconfig.json ├── .vscode └── settings.json ├── tsdown.config.ts ├── src ├── bundlers │ ├── BundlerOptions.ts │ ├── bunup.ts │ ├── tsup.ts │ ├── tsdown.ts │ ├── unbuild.ts │ └── rslib.ts ├── util │ ├── CommonUtil.ts │ └── MetricsUtil.ts ├── generate.ts └── benchmark.ts ├── README.md ├── biome.json ├── LICENSE ├── .github └── workflows │ └── publish-docs.yml └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /projects/thousand-functions/.gitignore: -------------------------------------------------------------------------------- 1 | src/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gugustinette/bundler-benchmark/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/ts-function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gugustinette/bundler-benchmark/HEAD/docs/public/ts-function.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[typescript][javascript][vue]": { 4 | "editor.defaultFormatter": "biomejs.biome" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsdown"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/benchmark.ts"], 5 | skipNodeModulesBundle: true, 6 | }); 7 | -------------------------------------------------------------------------------- /docs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "docs", 8 | "hasInstallScript": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /src/bundlers/BundlerOptions.ts: -------------------------------------------------------------------------------- 1 | export interface BundlerOptions { 2 | /** 3 | * The name of the project to bundle. 4 | */ 5 | project?: string; 6 | // General bundler options 7 | cjs?: boolean; 8 | minify?: boolean; 9 | sourcemap?: boolean; 10 | dts?: boolean; 11 | isolatedDeclarations?: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /projects/thousand-functions/rslib.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2021"], 4 | "module": "ESNext", 5 | "noEmit": true, 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "isolatedModules": true, 9 | "resolveJsonModule": true, 10 | "moduleResolution": "bundler", 11 | "useDefineForClassFields": true, 12 | "allowImportingTsExtensions": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /docs/composables/useBenchmarkData.ts: -------------------------------------------------------------------------------- 1 | export interface BenchmarkResults { 2 | [feature: string]: { 3 | [bundler: string]: { 4 | executionTime: number; // in milliseconds 5 | heapUsage: number; // in megabytes 6 | }; 7 | }; 8 | } 9 | 10 | export const useBenchmarkResults = async (): Promise => { 11 | // Get the benchmark results from the JSON file 12 | const response = await fetch( 13 | "/bundler-benchmark/results/benchmark-results.json", 14 | ); 15 | if (!response.ok) { 16 | throw new Error("Failed to fetch benchmark results"); 17 | } 18 | const benchmarkResults = await response.json(); 19 | 20 | // Return the benchmark results 21 | return benchmarkResults; 22 | }; 23 | -------------------------------------------------------------------------------- /src/bundlers/bunup.ts: -------------------------------------------------------------------------------- 1 | import { build as buildBunup } from "bunup"; 2 | import type { BundlerOptions } from "./BundlerOptions"; 3 | 4 | export const build = async (options: BundlerOptions) => { 5 | // Setup the project path 6 | const projectDir = `projects/${options.project}`; 7 | const entryFile = `${projectDir}/src/index.ts`; 8 | const outputDir = `${projectDir}/dist/bunup`; 9 | 10 | // Build the project 11 | await buildBunup({ 12 | entry: entryFile, 13 | outDir: outputDir, 14 | format: [options.cjs ? "cjs" : "esm"], 15 | clean: true, 16 | sourcemap: options.sourcemap ?? false, 17 | minify: options.minify ?? false, 18 | silent: true, 19 | dts: options.dts ?? false, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/bundlers/tsup.ts: -------------------------------------------------------------------------------- 1 | import { build as buildTsup } from "tsup"; 2 | import type { BundlerOptions } from "./BundlerOptions"; 3 | 4 | export const build = async (options: BundlerOptions) => { 5 | // Setup the project path 6 | const projectDir = `projects/${options.project}`; 7 | const entryFile = `${projectDir}/src/index.ts`; 8 | const outputDir = `${projectDir}/dist/tsup`; 9 | 10 | // Build the project 11 | await buildTsup({ 12 | entry: [entryFile], 13 | outDir: outputDir, 14 | format: options.cjs ? "cjs" : "esm", 15 | target: "esnext", 16 | clean: true, 17 | sourcemap: options.sourcemap || false, 18 | minify: options.minify || false, 19 | silent: true, 20 | dts: options.dts || false, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bundler-benchmark 2 | 3 | Benchmark some popular TS bundlers: 4 | 5 | - [tsdown](https://tsdown.dev/) 6 | - [tsup](https://tsup.egoist.dev/) 7 | - [unbuild](https://github.com/unjs/unbuild) 8 | - [rslib](https://lib.rsbuild.dev/) 9 | - [bunup](https://bunup.dev/) 10 | 11 | Results are available at [gugustinette.github.io/bundler-benchmark](https://gugustinette.github.io/bundler-benchmark/). 12 | 13 | ### Development 14 | 15 | - Install dependencies 16 | 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | - Generate the benchmark data 22 | 23 | ```bash 24 | npm run generate 25 | ``` 26 | 27 | - Build the benchmark code 28 | 29 | ```bash 30 | npm run build 31 | ``` 32 | 33 | - Run the benchmark 34 | 35 | ```bash 36 | npm run benchmark 37 | ``` 38 | -------------------------------------------------------------------------------- /src/bundlers/tsdown.ts: -------------------------------------------------------------------------------- 1 | import { build as buildTsdown } from "tsdown"; 2 | import type { BundlerOptions } from "./BundlerOptions"; 3 | 4 | export const build = async (options: BundlerOptions) => { 5 | // Setup the project path 6 | const projectDir = `projects/${options.project}`; 7 | const entryFile = `${projectDir}/src/index.ts`; 8 | const outputDir = `${projectDir}/dist/tsdown`; 9 | 10 | // Build the project 11 | await buildTsdown({ 12 | entry: entryFile, 13 | outDir: outputDir, 14 | format: options.cjs ? "cjs" : "esm", 15 | target: false, 16 | sourcemap: options.sourcemap ?? false, 17 | minify: options.minify ?? false, 18 | logLevel: "silent", 19 | dts: options.dts ? { oxc: options.isolatedDeclarations ?? true } : false, 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from "nuxt/config"; 2 | 3 | // https://nuxt.com/docs/api/configuration/nuxt-config 4 | export default defineNuxtConfig({ 5 | compatibilityDate: "2025-05-15", 6 | ssr: false, 7 | devtools: { enabled: true }, 8 | modules: ["@nuxt/fonts", "@nuxt/image"], 9 | app: { 10 | baseURL: "/bundler-benchmark", 11 | head: { 12 | title: "TS Bundler Benchmark", 13 | htmlAttrs: { 14 | lang: "en", 15 | }, 16 | link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }], 17 | }, 18 | }, 19 | fonts: { 20 | assets: { 21 | prefix: "/bundler-benchmark/_fonts/", 22 | }, 23 | }, 24 | nitro: { 25 | prerender: { 26 | routes: ["/_ipx/w_600/ts-function.png", "/_ipx/w_1200/ts-function.png"], 27 | }, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /src/bundlers/unbuild.ts: -------------------------------------------------------------------------------- 1 | import { build as buildUnbuild } from "unbuild"; 2 | import type { BundlerOptions } from "./BundlerOptions"; 3 | 4 | export const build = async (options: BundlerOptions) => { 5 | // Setup the project path 6 | const projectDir = `projects/${options.project}`; 7 | const entryFile = "src/index.ts"; 8 | const outputDir = "dist/unbuild"; 9 | 10 | // Build the project 11 | await buildUnbuild(projectDir, false, { 12 | entries: [entryFile], 13 | outDir: outputDir, 14 | clean: true, 15 | sourcemap: options.sourcemap || false, 16 | declaration: options.dts ? "node16" : false, 17 | rollup: { 18 | emitCJS: false, 19 | esbuild: { 20 | minify: options.minify || false, 21 | target: "esnext", 22 | format: options.cjs ? "cjs" : "esm", 23 | }, 24 | }, 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /docs/app.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "includes": [ 11 | "**", 12 | "!**/node_modules", 13 | "!**/dist", 14 | "!**/docs/node_modules", 15 | "!**/docs/package.json", 16 | "!**/docs/.nuxt", 17 | "!**/docs/.output", 18 | "!**/docs/public", 19 | "!**/projects" 20 | ] 21 | }, 22 | "formatter": { 23 | "enabled": true, 24 | "indentStyle": "tab" 25 | }, 26 | "assist": { 27 | "actions": { 28 | "source": { 29 | "organizeImports": "on" 30 | } 31 | } 32 | }, 33 | "linter": { 34 | "enabled": true, 35 | "rules": { 36 | "recommended": true, 37 | "complexity": { 38 | "noForEach": "off", 39 | "noStaticOnlyClass": "off" 40 | } 41 | } 42 | }, 43 | "javascript": { 44 | "formatter": { 45 | "quoteStyle": "double" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/bundlers/rslib.ts: -------------------------------------------------------------------------------- 1 | import { build as buildRslib } from "@rslib/core"; 2 | import type { BundlerOptions } from "./BundlerOptions"; 3 | 4 | export const build = async (options: BundlerOptions) => { 5 | // Setup the project path 6 | const projectDir = `projects/${options.project}`; 7 | const entryFile = `${projectDir}/src/index.ts`; 8 | const outputDir = `${projectDir}/dist/rslib`; 9 | 10 | // Build the project 11 | await buildRslib({ 12 | lib: [ 13 | { 14 | source: { 15 | entry: { 16 | index: entryFile, 17 | }, 18 | tsconfigPath: options.dts 19 | ? `${projectDir}/rslib.tsconfig.json` 20 | : undefined, 21 | }, 22 | format: options.cjs ? "cjs" : "esm", 23 | dts: options.dts || false, 24 | output: { 25 | minify: options.minify || false, 26 | sourceMap: options.sourcemap || false, 27 | distPath: { 28 | root: outputDir, 29 | }, 30 | cleanDistPath: true, 31 | }, 32 | }, 33 | ], 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Gugustinette 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/composables/utils/CommonUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Deeply merges objects together, preserving nested properties 3 | * @param target The target object 4 | * @param sources One or more source objects to merge 5 | * @returns The merged object 6 | */ 7 | // biome-ignore lint/suspicious/noExplicitAny: Required for generic deep merge utility 8 | export function deepMerge>( 9 | target: T, 10 | ...sources: Partial[] 11 | ): T { 12 | if (!sources.length) return target; 13 | 14 | const source = sources.shift(); 15 | if (!source) return target; 16 | 17 | for (const key in source) { 18 | if ( 19 | source[key] && 20 | typeof source[key] === "object" && 21 | !Array.isArray(source[key]) 22 | ) { 23 | if (!target[key] || typeof target[key] !== "object") { 24 | // biome-ignore lint/suspicious/noExplicitAny: Required for generic deep merge utility 25 | target[key] = {} as any; 26 | } 27 | deepMerge( 28 | // biome-ignore lint/suspicious/noExplicitAny: Required for generic deep merge utility 29 | target[key] as Record, 30 | // biome-ignore lint/suspicious/noExplicitAny: Required for generic deep merge utility 31 | source[key] as Record, 32 | ); 33 | } else if (source[key] !== undefined) { 34 | target[key] = source[key]; 35 | } 36 | } 37 | 38 | return deepMerge(target, ...sources); 39 | } 40 | -------------------------------------------------------------------------------- /src/util/CommonUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility class for common operations 3 | */ 4 | export class CommonUtil { 5 | /** 6 | * Deeply merges objects together, preserving nested properties 7 | * @param target The target object 8 | * @param sources One or more source objects to merge 9 | * @returns The merged object 10 | */ 11 | // biome-ignore lint/suspicious/noExplicitAny: Required for generic deep merge utility 12 | static deepMerge>( 13 | target: T, 14 | ...sources: Partial[] 15 | ): T { 16 | if (!sources.length) return target; 17 | 18 | const source = sources.shift(); 19 | if (!source) return target; 20 | 21 | for (const key in source) { 22 | if ( 23 | source[key] && 24 | typeof source[key] === "object" && 25 | !Array.isArray(source[key]) 26 | ) { 27 | if (!target[key] || typeof target[key] !== "object") { 28 | // biome-ignore lint/suspicious/noExplicitAny: Required for generic deep merge utility 29 | target[key] = {} as any; 30 | } 31 | CommonUtil.deepMerge( 32 | // biome-ignore lint/suspicious/noExplicitAny: Required for generic deep merge utility 33 | target[key] as Record, 34 | // biome-ignore lint/suspicious/noExplicitAny: Required for generic deep merge utility 35 | source[key] as Record, 36 | ); 37 | } else if (source[key] !== undefined) { 38 | target[key] = source[key]; 39 | } 40 | } 41 | 42 | return CommonUtil.deepMerge(target, ...sources); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/deploy-pages#usage 2 | name: Deploy to GitHub Pages 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - 'docs/**' 11 | - '.github/workflows/publish-docs.yml' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 18 | - run: corepack enable 19 | - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 20 | with: 21 | node-version: 'lts/*' 22 | - run: npm install --ignore-scripts 23 | - run: npm run docs:generate 24 | - name: Upload artifact 25 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 26 | with: 27 | path: ./docs/.output/public 28 | # Deployment job 29 | deploy: 30 | # Add a dependency to the build job 31 | needs: build 32 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 33 | permissions: 34 | pages: write # to deploy to Pages 35 | id-token: write # to verify the deployment originates from an appropriate source 36 | # Deploy to the github_pages environment 37 | environment: 38 | name: github_pages 39 | url: ${{ steps.deployment.outputs.page_url }} 40 | # Specify runner + deployment step 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Deploy to GitHub Pages 44 | id: deployment 45 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bundler-benchmark", 3 | "version": "0.0.1", 4 | "description": "Benchmark some popular TS bundler", 5 | "author": "Gugustinette", 6 | "type": "module", 7 | "scripts": { 8 | "install": "cd docs && npm install", 9 | "updateBundlers": "npm install -D @rslib/core@latest tsdown@latest tsup@latest unbuild@latest bunup@latest", 10 | "build": "tsdown", 11 | "dev": "npm run build && node dist/benchmark.mjs", 12 | "dev:bun": "npm run build && bun dist/benchmark.mjs --runtime bun", 13 | "benchmark": "CONSOLA_LEVEL=0 node dist/benchmark.mjs && bun dist/benchmark.mjs --runtime bun", 14 | "generate": "tsx src/generate.ts", 15 | "lint:fix": "biome check --fix --unsafe", 16 | "lint": "biome check", 17 | "format:fix": "biome format --write", 18 | "format": "biome format", 19 | "docs:build": "cd docs && npm run build", 20 | "docs:dev": "cd docs && npm run dev", 21 | "docs:generate": "cd docs && npm run generate", 22 | "docs:preview": "cd docs && npm run preview", 23 | "docs:postinstall": "cd docs && npm run postinstall" 24 | }, 25 | "keywords": [], 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@biomejs/biome": "2.3.8", 29 | "@nuxt/fonts": "^0.12.1", 30 | "@nuxt/image": "^2.0.0", 31 | "@rslib/core": "^0.18.2", 32 | "@types/node": "^24.10.1", 33 | "bunup": "^0.16.10", 34 | "chart.js": "^4.5.1", 35 | "mitata": "^1.0.34", 36 | "nuxt": "^4.2.1", 37 | "tsdown": "^0.17.0-beta.3", 38 | "tsup": "^8.5.1", 39 | "tsx": "^4.20.6", 40 | "typescript": "^5.9.3", 41 | "unbuild": "^3.6.1", 42 | "vue": "^3.5.25", 43 | "vue-router": "^4.6.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/composables/utils/ChartUtil.ts: -------------------------------------------------------------------------------- 1 | export const COMMON_DATASET_OPTIONS = { 2 | backgroundColor: [ 3 | "rgba(255, 99, 132, 0.2)", 4 | "rgba(255, 159, 64, 0.2)", 5 | "rgba(255, 205, 86, 0.2)", 6 | "rgba(75, 192, 192, 0.2)", 7 | "rgba(54, 162, 235, 0.2)", 8 | "rgba(153, 102, 255, 0.2)", 9 | "rgba(201, 203, 207, 0.2)", 10 | ], 11 | borderColor: [ 12 | "rgb(255, 99, 132)", 13 | "rgb(255, 159, 64)", 14 | "rgb(255, 205, 86)", 15 | "rgb(75, 192, 192)", 16 | "rgb(54, 162, 235)", 17 | "rgb(153, 102, 255)", 18 | "rgb(201, 203, 207)", 19 | ], 20 | borderWidth: 1, 21 | }; 22 | 23 | // Function to create consistent color mapping for items within the bubble chart 24 | export const createItemColorMapping = (items: string[]) => { 25 | const colorMapping: Record = 26 | {}; 27 | 28 | items.forEach((item, index) => { 29 | const colorIndex = index % COMMON_DATASET_OPTIONS.backgroundColor.length; 30 | colorMapping[item] = { 31 | background: COMMON_DATASET_OPTIONS.backgroundColor[colorIndex], 32 | border: COMMON_DATASET_OPTIONS.borderColor[colorIndex], 33 | }; 34 | }); 35 | 36 | return colorMapping; 37 | }; 38 | 39 | export const COMMON_CHART_OPTIONS = { 40 | responsive: true, 41 | maintainAspectRatio: false, 42 | plugins: { 43 | legend: { 44 | display: true, 45 | labels: { 46 | color: "#888", 47 | usePointStyle: true, 48 | pointStyle: "rect", 49 | }, 50 | }, 51 | tooltip: { 52 | backgroundColor: "rgba(0, 0, 0, 0.8)", 53 | titleColor: "#fff", 54 | bodyColor: "#fff", 55 | }, 56 | }, 57 | scales: { 58 | x: { 59 | grid: { 60 | color: "rgba(200, 200, 200, 0.1)", 61 | }, 62 | ticks: { 63 | color: "#888", 64 | }, 65 | title: { 66 | display: true, 67 | color: "#888", 68 | }, 69 | }, 70 | y: { 71 | grid: { 72 | color: "rgba(200, 200, 200, 0.1)", 73 | }, 74 | ticks: { 75 | color: "#888", 76 | }, 77 | title: { 78 | display: true, 79 | text: "Y Axis Title", 80 | color: "#888", 81 | }, 82 | }, 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /docs/composables/updateBubbleChart.ts: -------------------------------------------------------------------------------- 1 | import type { Chart } from "chart.js"; 2 | import type { BenchmarkResults } from "./useBenchmarkData"; 3 | import { createItemColorMapping } from "./utils/ChartUtil"; 4 | 5 | interface BubbleChartOptions { 6 | chart: Chart; 7 | data: BenchmarkResults; 8 | feature: string; 9 | } 10 | 11 | export const updateBubbleChart = ({ 12 | chart, 13 | data, 14 | feature, 15 | }: BubbleChartOptions): void => { 16 | // Get the feature data 17 | const featureData = data[feature]; 18 | if (!featureData) { 19 | console.error(`Feature "${feature}" not found in benchmark data`); 20 | // Optionally clear the chart or display an error message within the chart 21 | chart.data.datasets = []; 22 | chart.update(); 23 | return; 24 | } 25 | 26 | // Get all bundlers for this feature 27 | const bundlers = Object.keys(featureData); 28 | 29 | // Create consistent color mapping for bundlers (important to maintain consistency on updates) 30 | const bundlerColorMapping = createItemColorMapping(bundlers); 31 | 32 | // Create new datasets 33 | const datasets = bundlers.map((bundler) => { 34 | const result = featureData[bundler]; 35 | 36 | return { 37 | label: bundler, // Dataset label is the bundler name 38 | data: [ 39 | { 40 | x: result.heapUsage, 41 | y: result.executionTime, 42 | r: 8, 43 | bundlerLabel: bundler, // Store bundler name for tooltips 44 | }, 45 | ], 46 | backgroundColor: bundlerColorMapping[bundler].background, 47 | borderColor: bundlerColorMapping[bundler].border, 48 | borderWidth: 1, 49 | }; 50 | }); 51 | 52 | // Update the chart's data 53 | chart.data.datasets = datasets; 54 | 55 | // Update the chart options if necessary (e.g., the tooltip title which includes the feature) 56 | if (chart.options.plugins?.tooltip?.callbacks) { 57 | // biome-ignore lint/suspicious/noExplicitAny: No type information available 58 | chart.options.plugins.tooltip.callbacks.title = (context: any) => { 59 | return `Feature: ${feature}`; 60 | }; 61 | } 62 | 63 | // Redraw the chart 64 | chart.update(); 65 | }; 66 | -------------------------------------------------------------------------------- /src/util/MetricsUtil.ts: -------------------------------------------------------------------------------- 1 | import { CommonUtil } from "./CommonUtil"; 2 | 3 | export interface BenchmarkResults { 4 | [feature: string]: { 5 | [bundler: string]: { 6 | executionTime: number; // in milliseconds 7 | heapUsage: number; // in megabytes 8 | }; 9 | }; 10 | } 11 | 12 | /** 13 | * Utility class for handling metrics 14 | */ 15 | export class MetricsUtil { 16 | /** 17 | * Save given metrics to a JSON file 18 | * @param metrics Object containing benchmark results 19 | * @param filePath Path to save the JSON file 20 | */ 21 | public static saveMetricsToJson( 22 | metrics: BenchmarkResults, 23 | filePath: string, 24 | ): void { 25 | try { 26 | // Check if we're in a Node.js environment 27 | if (typeof require !== "undefined") { 28 | const fs = require("node:fs"); 29 | let existingData: BenchmarkResults = {}; 30 | if (fs.existsSync(filePath)) { 31 | const fileContent = fs.readFileSync(filePath, "utf8"); 32 | existingData = JSON.parse(fileContent); 33 | } 34 | 35 | const mergedData = CommonUtil.deepMerge(existingData, metrics); 36 | 37 | fs.writeFileSync(filePath, JSON.stringify(mergedData, null, 2), "utf8"); 38 | } else { 39 | throw new Error( 40 | "This method can only be used in a Node.js environment", 41 | ); 42 | } 43 | } catch (error) { 44 | console.error("Error saving metrics to JSON file:", error); 45 | throw error; 46 | } 47 | } 48 | 49 | /** 50 | * Log the results of the benchmark 51 | * @param metrics Object containing project metrics data 52 | */ 53 | public static logMetrics(metrics: BenchmarkResults): void { 54 | console.log("Benchmark Results:"); 55 | for (const project in metrics) { 56 | console.log(`Project: ${project}`); 57 | for (const bundler in metrics[project]) { 58 | const { executionTime, heapUsage } = metrics[project][bundler]; 59 | console.log( 60 | ` Bundler: ${bundler}, Execution Time: ${executionTime.toFixed( 61 | 2, 62 | )} ms, Heap Usage: ${(heapUsage).toFixed(2)} MB`, 63 | ); 64 | } 65 | } 66 | console.log(""); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/composables/updateBarChart.ts: -------------------------------------------------------------------------------- 1 | import type { Chart } from "chart.js"; 2 | import type { BenchmarkResults } from "./useBenchmarkData"; 3 | import { createItemColorMapping } from "./utils/ChartUtil"; 4 | 5 | interface BarChartOptions { 6 | chart: Chart; 7 | data: BenchmarkResults; 8 | feature: string; 9 | measurement: "executionTime" | "heapUsage"; 10 | } 11 | 12 | export const updateBarChart = ({ 13 | chart, 14 | data, 15 | feature, 16 | measurement, 17 | }: BarChartOptions): void => { 18 | // Get the feature data 19 | const featureData = data[feature]; 20 | if (!featureData) { 21 | console.error(`Feature "${feature}" not found in benchmark data`); 22 | // Optionally clear the chart or display an error message within the chart 23 | chart.data.datasets = []; 24 | chart.update(); 25 | return; 26 | } 27 | 28 | // Get all bundlers for this feature 29 | const bundlers = Object.keys(featureData); 30 | 31 | // Infer measurement details from the measurement type 32 | const measurementConfig = { 33 | executionTime: { 34 | yAxisTitle: "Execution Time (ms)", 35 | unit: "ms", 36 | }, 37 | heapUsage: { 38 | yAxisTitle: "Heap Usage (MB)", 39 | unit: "MB", 40 | }, 41 | }; 42 | 43 | const config = measurementConfig[measurement]; 44 | 45 | // Create consistent color mapping for bundlers (important to maintain consistency on updates) 46 | const bundlerColorMapping = createItemColorMapping(bundlers); 47 | 48 | // Create new datasets 49 | const datasets = bundlers.map((bundler) => ({ 50 | label: bundler, 51 | data: [featureData[bundler][measurement]], // Single value array for this bundler 52 | backgroundColor: bundlerColorMapping[bundler].background, 53 | borderColor: bundlerColorMapping[bundler].border, 54 | borderWidth: 1, 55 | })); 56 | 57 | // Update the chart's data and labels 58 | chart.data.labels = [""]; // Keep a single empty label 59 | chart.data.datasets = datasets; 60 | 61 | // Update the chart options for the Y-axis title and tooltip 62 | if (chart.options.scales?.y) { 63 | // biome-ignore lint/suspicious/noExplicitAny: No type information available 64 | const yScale = chart.options.scales.y as any; 65 | if (yScale.title) { 66 | yScale.title.text = config.yAxisTitle; 67 | } 68 | } 69 | 70 | if (chart.options.plugins?.tooltip?.callbacks) { 71 | // biome-ignore lint/suspicious/noExplicitAny: No type information available 72 | chart.options.plugins.tooltip.callbacks.label = (context: any) => { 73 | const bundlerLabel = context.dataset.label || ""; 74 | const yValue = context.parsed.y; 75 | return `${bundlerLabel}: ${Number(yValue).toFixed(0)}${config.unit}`; 76 | }; 77 | // biome-ignore lint/suspicious/noExplicitAny: No type information available 78 | chart.options.plugins.tooltip.callbacks.title = (context: any) => { 79 | return `Feature: ${feature}`; 80 | }; 81 | } 82 | 83 | // Redraw the chart 84 | chart.update(); 85 | }; 86 | -------------------------------------------------------------------------------- /docs/composables/renderBarChart.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BarController, 3 | BarElement, 4 | CategoryScale, 5 | Chart, 6 | Legend, 7 | LinearScale, 8 | Tooltip, 9 | } from "chart.js"; 10 | import type { BenchmarkResults } from "./useBenchmarkData"; 11 | import { 12 | COMMON_CHART_OPTIONS, 13 | createItemColorMapping, 14 | } from "./utils/ChartUtil"; 15 | import { deepMerge } from "./utils/CommonUtil"; 16 | 17 | Chart.register( 18 | CategoryScale, 19 | LinearScale, 20 | BarController, 21 | BarElement, 22 | Tooltip, 23 | Legend, 24 | ); 25 | 26 | interface BarChartOptions { 27 | canvas: HTMLCanvasElement; 28 | data: BenchmarkResults; 29 | feature: string; 30 | measurement: "executionTime" | "heapUsage"; 31 | } 32 | 33 | export const renderBarChart = ({ 34 | canvas, 35 | data, 36 | feature, 37 | measurement, 38 | }: BarChartOptions): Chart => { 39 | // Get the feature data 40 | const featureData = data[feature]; 41 | if (!featureData) { 42 | throw new Error(`Feature "${feature}" not found in benchmark data`); 43 | } 44 | 45 | // Get all bundlers for this feature 46 | const bundlers = Object.keys(featureData); 47 | 48 | // Infer measurement details from the measurement type 49 | const measurementConfig = { 50 | executionTime: { 51 | yAxisTitle: "Execution Time (ms)", 52 | unit: "ms", 53 | }, 54 | heapUsage: { 55 | yAxisTitle: "Heap Usage (MB)", 56 | unit: "MB", 57 | }, 58 | }; 59 | 60 | const config = measurementConfig[measurement]; 61 | 62 | // Create consistent color mapping for bundlers 63 | const bundlerColorMapping = createItemColorMapping(bundlers); 64 | 65 | // Create one dataset per bundler so each can be toggled individually 66 | const datasets = bundlers.map((bundler) => ({ 67 | label: bundler, 68 | data: [featureData[bundler][measurement]], // Single value array for this bundler 69 | backgroundColor: bundlerColorMapping[bundler].background, 70 | borderColor: bundlerColorMapping[bundler].border, 71 | borderWidth: 1, 72 | })); 73 | 74 | const chartData = { 75 | labels: [""], // Single empty label since we're using multiple datasets 76 | datasets: datasets, 77 | }; 78 | 79 | return new Chart(canvas, { 80 | type: "bar", 81 | data: chartData, 82 | options: deepMerge({}, COMMON_CHART_OPTIONS, { 83 | scales: { 84 | y: { 85 | title: { 86 | text: config.yAxisTitle, 87 | display: true, 88 | color: "#888", 89 | }, 90 | beginAtZero: true, 91 | }, 92 | }, 93 | plugins: { 94 | tooltip: { 95 | callbacks: { 96 | // biome-ignore lint/suspicious/noExplicitAny: No type information available 97 | label: (context: any) => { 98 | const bundlerLabel = context.dataset.label || ""; 99 | const yValue = context.parsed.y; 100 | return `${bundlerLabel}: ${Number(yValue).toFixed(0)}${config.unit}`; 101 | }, 102 | // biome-ignore lint/suspicious/noExplicitAny: No type information available 103 | title: (context: any) => { 104 | return `Feature: ${feature}`; 105 | }, 106 | }, 107 | }, 108 | }, 109 | }), 110 | }); 111 | }; 112 | -------------------------------------------------------------------------------- /docs/public/results/benchmark-results.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "unbuild": { 4 | "executionTime": 717.72242025, 5 | "heapUsage": 124.31220531463623 6 | }, 7 | "tsup": { 8 | "executionTime": 74.755757, 9 | "heapUsage": 11.813771565755209 10 | }, 11 | "tsdown": { 12 | "executionTime": 35.9231118125, 13 | "heapUsage": 6.622735864975873 14 | }, 15 | "rslib": { 16 | "executionTime": 240.0999445, 17 | "heapUsage": 3.2006454467773438 18 | }, 19 | "bunup": { 20 | "executionTime": 23.398683518518517, 21 | "heapUsage": 1.6265120967741935 22 | } 23 | }, 24 | "cjs": { 25 | "unbuild": { 26 | "executionTime": 749.8450380833334, 27 | "heapUsage": 144.51992458767361 28 | }, 29 | "tsup": { 30 | "executionTime": 74.69061475, 31 | "heapUsage": 11.665211147732204 32 | }, 33 | "tsdown": { 34 | "executionTime": 36.82413046666667, 35 | "heapUsage": 6.619632720947266 36 | }, 37 | "rslib": { 38 | "executionTime": 231.74194124999937, 39 | "heapUsage": 4.479310469193892 40 | }, 41 | "bunup": { 42 | "executionTime": 23.36374676923077, 43 | "heapUsage": 0.4791666666666667 44 | } 45 | }, 46 | "minify": { 47 | "unbuild": { 48 | "executionTime": 743.8156113333333, 49 | "heapUsage": 120.58306630452473 50 | }, 51 | "tsup": { 52 | "executionTime": 73.53076741666668, 53 | "heapUsage": 11.521665785047743 54 | }, 55 | "tsdown": { 56 | "executionTime": 38.8441965, 57 | "heapUsage": 6.51007080078125 58 | }, 59 | "rslib": { 60 | "executionTime": 258.534264, 61 | "heapUsage": 2.9288101196289062 62 | }, 63 | "bunup": { 64 | "executionTime": 23.28419448148148, 65 | "heapUsage": 0.3634072580645161 66 | } 67 | }, 68 | "sourcemap": { 69 | "unbuild": { 70 | "executionTime": 714.1756041666666, 71 | "heapUsage": 140.39183383517795 72 | }, 73 | "tsup": { 74 | "executionTime": 82.37034733333333, 75 | "heapUsage": 15.374785529242622 76 | }, 77 | "tsdown": { 78 | "executionTime": 39.25730064285714, 79 | "heapUsage": 6.617115497589111 80 | }, 81 | "rslib": { 82 | "executionTime": 239.02557608333333, 83 | "heapUsage": 3.19467509876598 84 | }, 85 | "bunup": { 86 | "executionTime": 38.50167486666667, 87 | "heapUsage": 4.876644736842105 88 | } 89 | }, 90 | "dts": { 91 | "unbuild": { 92 | "executionTime": 2554.799117916666, 93 | "heapUsage": 424.81669902801514 94 | }, 95 | "tsup": { 96 | "executionTime": 2453.28513525, 97 | "heapUsage": 11.789594014485678 98 | }, 99 | "tsdown (oxc)": { 100 | "executionTime": 402.98607641666285, 101 | "heapUsage": 81.00088570334695 102 | }, 103 | "rslib": { 104 | "executionTime": 1214.2036560833333, 105 | "heapUsage": 9.32757568359375 106 | }, 107 | "bunup": { 108 | "executionTime": 302.7250625, 109 | "heapUsage": 5.888020833333333 110 | }, 111 | "tsdown (tsc)": { 112 | "executionTime": 560.70100375, 113 | "heapUsage": 93.00636800130208 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /docs/composables/renderBubbleChart.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BubbleController, 3 | Chart, 4 | Legend, 5 | LinearScale, 6 | PointElement, 7 | Tooltip, 8 | } from "chart.js"; 9 | import type { BenchmarkResults } from "./useBenchmarkData"; 10 | import { 11 | COMMON_CHART_OPTIONS, 12 | createItemColorMapping, 13 | } from "./utils/ChartUtil"; 14 | import { deepMerge } from "./utils/CommonUtil"; 15 | 16 | // Register the necessary chart components for bubble charts 17 | Chart.register(LinearScale, BubbleController, PointElement, Tooltip, Legend); 18 | 19 | interface BubbleChartOptions { 20 | canvas: HTMLCanvasElement; 21 | data: BenchmarkResults; 22 | feature: string; 23 | } 24 | 25 | export const renderBubbleChart = ({ 26 | canvas, 27 | data, 28 | feature, 29 | }: BubbleChartOptions): Chart => { 30 | // Get the feature data 31 | const featureData = data[feature]; 32 | if (!featureData) { 33 | throw new Error(`Feature "${feature}" not found in benchmark data`); 34 | } 35 | 36 | // Get all bundlers for this feature 37 | const bundlers = Object.keys(featureData); 38 | 39 | // Create consistent color mapping for bundlers 40 | const bundlerColorMapping = createItemColorMapping(bundlers); 41 | 42 | // Create datasets: each bundler becomes a dataset with a single data point 43 | const datasets = bundlers.map((bundler) => { 44 | const result = featureData[bundler]; 45 | 46 | return { 47 | label: bundler, // Dataset label is the bundler name 48 | data: [ 49 | { 50 | x: result.heapUsage, 51 | y: result.executionTime, 52 | r: 8, 53 | bundlerLabel: bundler, // Store bundler name for tooltips 54 | }, 55 | ], 56 | backgroundColor: bundlerColorMapping[bundler].background, 57 | borderColor: bundlerColorMapping[bundler].border, 58 | borderWidth: 1, 59 | }; 60 | }); 61 | 62 | return new Chart(canvas, { 63 | type: "bubble", 64 | data: { 65 | datasets: datasets, 66 | }, 67 | options: deepMerge({}, COMMON_CHART_OPTIONS, { 68 | plugins: { 69 | legend: { 70 | display: true, // Enable the legend 71 | position: "top", 72 | labels: { 73 | color: "#888", 74 | usePointStyle: true, // Use circle points in legend 75 | pointStyle: "circle", 76 | }, 77 | }, 78 | tooltip: { 79 | callbacks: { 80 | // biome-ignore lint/suspicious/noExplicitAny: No type information available 81 | label: (context: any) => { 82 | const bundlerLabel = context.dataset.label || ""; 83 | const xValue = context.parsed.x; 84 | const yValue = context.parsed.y; 85 | return `${bundlerLabel}: Time=${Number(yValue).toFixed(0)}ms, Heap=${Number(xValue).toFixed(0)}MB`; 86 | }, 87 | // biome-ignore lint/suspicious/noExplicitAny: No type information available 88 | title: (context: any) => { 89 | return `Feature: ${feature}`; 90 | }, 91 | }, 92 | }, 93 | }, 94 | scales: { 95 | x: { 96 | title: { 97 | text: "Heap Usage (MB)", 98 | display: true, 99 | color: "#888", 100 | }, 101 | type: "linear", 102 | position: "bottom", 103 | }, 104 | y: { 105 | title: { 106 | text: "Execution Time (ms)", 107 | display: true, 108 | color: "#888", 109 | }, 110 | type: "linear", 111 | beginAtZero: true, 112 | }, 113 | }, 114 | }), 115 | }); 116 | }; 117 | -------------------------------------------------------------------------------- /src/generate.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | // Project types and configuration 5 | interface ProjectConfig { 6 | name: string; 7 | basePath: string; 8 | count: number; 9 | generateFileContent: (index: number) => string; 10 | getFileName: (index: number) => string; 11 | } 12 | 13 | // Utility functions 14 | const createDirectoryIfNotExists = async (dirPath: string) => { 15 | await fs.promises.mkdir(dirPath, { recursive: true }); 16 | }; 17 | 18 | const cleanDirectory = async (dirPath: string, excludeFiles: string[] = []) => { 19 | const files = await fs.promises.readdir(dirPath); 20 | for (const file of files) { 21 | if (!excludeFiles.includes(file)) { 22 | await fs.promises.unlink(path.join(dirPath, file)); 23 | } 24 | } 25 | }; 26 | 27 | const generateIndexFile = async ( 28 | files: { index: number; fileName: string }[], 29 | outputPath: string, 30 | ) => { 31 | const indexContent = files 32 | .map(({ fileName }) => `export * from './${fileName.replace(".ts", "")}';`) 33 | .join("\n"); 34 | await fs.promises.writeFile(outputPath, `${indexContent}\n`); 35 | }; 36 | 37 | // Content generator 38 | const generateFunction = (index: number): string => { 39 | const functionName = `myFunction${index}`; 40 | const interfaceName = `Options${index}`; 41 | 42 | // Generate interface with 50 optional attributes 43 | let interfaceContent = `interface ${interfaceName} {\n`; 44 | for (let i = 1; i <= 50; i++) { 45 | interfaceContent += ` prop${i}?: string;\n`; 46 | } 47 | interfaceContent += "}\n\n"; 48 | 49 | return `${interfaceContent}export const ${functionName} = (options?: ${interfaceName}): void => { 50 | console.log('Hello from function ${index}!', options); 51 | }; 52 | `; 53 | }; 54 | 55 | // Project generator 56 | const generateProject = async (config: ProjectConfig) => { 57 | const { name, basePath, count, generateFileContent, getFileName } = config; 58 | 59 | console.log(`Generating project: ${name}`); 60 | const projectDir = path.join(basePath, name, "src"); 61 | const indexFilePath = path.join(projectDir, "index.ts"); 62 | 63 | try { 64 | // Setup directory 65 | await createDirectoryIfNotExists(projectDir); 66 | await cleanDirectory(projectDir, ["index.ts"]); 67 | 68 | const generatedFiles: { index: number; fileName: string }[] = []; 69 | 70 | // Generate function files 71 | for (let i = 1; i <= count; i++) { 72 | const fileName = getFileName(i); 73 | const filePath = path.join(projectDir, fileName); 74 | const fileContent = generateFileContent(i); 75 | 76 | await fs.promises.writeFile(filePath, fileContent); 77 | generatedFiles.push({ index: i, fileName }); 78 | } 79 | 80 | // Generate index file 81 | await generateIndexFile(generatedFiles, indexFilePath); 82 | 83 | console.log(`Successfully generated ${count} files for ${name}`); 84 | } catch (error) { 85 | console.error(`Error generating project ${name}:`, error); 86 | throw error; 87 | } 88 | }; 89 | 90 | // Main function 91 | const generate = async () => { 92 | const basePath = path.join(import.meta.dirname, "../", "projects"); 93 | 94 | const projects: ProjectConfig[] = [ 95 | { 96 | name: "thousand-functions", 97 | basePath, 98 | count: 1000, 99 | generateFileContent: generateFunction, 100 | getFileName: (index) => `function${index}.ts`, 101 | }, 102 | ]; 103 | 104 | for (const project of projects) { 105 | await generateProject(project); 106 | } 107 | }; 108 | 109 | generate().catch((err) => { 110 | console.error("Error generating files:", err); 111 | process.exit(1); 112 | }); 113 | -------------------------------------------------------------------------------- /docs/components/Tooltip.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 78 | 79 | 137 | -------------------------------------------------------------------------------- /docs/pages/index.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 123 | 124 | 140 | -------------------------------------------------------------------------------- /src/benchmark.ts: -------------------------------------------------------------------------------- 1 | import { bench, run } from "mitata"; 2 | import type { BundlerOptions } from "./bundlers/BundlerOptions"; 3 | import { build as buildRslib } from "./bundlers/rslib"; 4 | import { build as buildTsdown } from "./bundlers/tsdown"; 5 | import { build as buildTsup } from "./bundlers/tsup"; 6 | import { build as buildUnbuild } from "./bundlers/unbuild"; 7 | import { type BenchmarkResults, MetricsUtil } from "./util/MetricsUtil"; 8 | import { build as buildBunup } from "./bundlers/bunup"; 9 | 10 | // Define bundlers declaratively 11 | let bundlers = [ 12 | { 13 | name: "unbuild", 14 | build: buildUnbuild, 15 | bundlerOptions: {}, 16 | }, 17 | { 18 | name: "tsup", 19 | build: buildTsup, 20 | bundlerOptions: {}, 21 | }, 22 | { 23 | name: "tsdown", 24 | build: buildTsdown, 25 | bundlerOptions: {}, 26 | }, 27 | { 28 | name: "rslib", 29 | build: buildRslib, 30 | bundlerOptions: {}, 31 | }, 32 | { 33 | name: "bunup", 34 | build: buildBunup, 35 | bundlerOptions: {}, 36 | }, 37 | ]; 38 | 39 | interface FeatureOptions { 40 | name: string; 41 | project: string; 42 | bundlerOptions?: BundlerOptions; 43 | // biome-ignore lint/suspicious/noExplicitAny: 44 | alterBundlerArray?: (bundlers: any[]) => any[]; 45 | } 46 | const features: FeatureOptions[] = [ 47 | { 48 | name: "default", 49 | project: "thousand-functions", 50 | bundlerOptions: {}, 51 | }, 52 | { 53 | name: "cjs", 54 | project: "thousand-functions", 55 | bundlerOptions: { 56 | cjs: true, 57 | }, 58 | }, 59 | { 60 | name: "minify", 61 | project: "thousand-functions", 62 | bundlerOptions: { 63 | minify: true, 64 | }, 65 | }, 66 | { 67 | name: "sourcemap", 68 | project: "thousand-functions", 69 | bundlerOptions: { 70 | sourcemap: true, 71 | }, 72 | }, 73 | { 74 | name: "dts", 75 | project: "thousand-functions", 76 | bundlerOptions: { 77 | dts: true, 78 | }, 79 | alterBundlerArray: (bundlers) => { 80 | // Verify tsdown is included 81 | if (!bundlers.some((b) => b.name === "tsdown")) { 82 | // Return the original array if tsdown is not found 83 | return bundlers; 84 | } 85 | // Modify the name of the existing tsdown entry 86 | const alteredBundlers = bundlers.map((bundler) => { 87 | if (bundler.name === "tsdown") { 88 | return { 89 | ...bundler, 90 | name: "tsdown (oxc)", 91 | }; 92 | } 93 | return bundler; 94 | }); 95 | // Duplicate the tsdown to test tsc usage 96 | alteredBundlers.push({ 97 | name: "tsdown (tsc)", 98 | build: buildTsdown, 99 | bundlerOptions: { 100 | isolatedDeclarations: false, 101 | }, 102 | }); 103 | return alteredBundlers; 104 | }, 105 | }, 106 | ]; 107 | 108 | const benchmark = async () => { 109 | // Create the metrics object 110 | const benchmarkResults: BenchmarkResults = {}; 111 | 112 | for (const feature of features) { 113 | // Initialize the map for the project 114 | benchmarkResults[feature.name] = {}; 115 | 116 | // Use the original bundler if no alteration is needed 117 | let bundlersToUse = bundlers; 118 | // Check if alterBundlerArray is defined and apply it 119 | if (feature.alterBundlerArray) { 120 | bundlersToUse = feature.alterBundlerArray(bundlers); 121 | } 122 | 123 | // Add the build functions to the benchmark suite using the bundlers array 124 | for (const bundler of bundlersToUse) { 125 | bench(`${feature.name}@${bundler.name}`, async () => { 126 | await bundler.build({ 127 | project: feature.project, 128 | ...feature.bundlerOptions, 129 | ...bundler.bundlerOptions, 130 | }); 131 | }); 132 | } 133 | } 134 | 135 | // Run the benchmark 136 | const results = await run(); 137 | 138 | // Convert the results to the custom metrics object 139 | results.benchmarks.forEach((benchmark) => { 140 | const name = benchmark.alias; 141 | const featureName = name.split("@")[0]; 142 | const bundlerName = name.split("@")[1]; 143 | if (benchmark.runs.length > 0) { 144 | const run = benchmark.runs[0]; 145 | const averageExecutionTime = run.stats?.avg ?? 0; // Execution time in nanoseconds 146 | const averageHeapUsage = run.stats?.heap?.avg ?? 0; // Heap usage in bytes 147 | benchmarkResults[featureName][bundlerName] = { 148 | executionTime: averageExecutionTime / 1e6, // Convert to milliseconds 149 | heapUsage: averageHeapUsage / 1024 / 1024, // Convert to megabytes 150 | }; 151 | } 152 | }); 153 | 154 | // Log the metrics to the console 155 | MetricsUtil.logMetrics(benchmarkResults); 156 | 157 | // Save the SVG chart to a file 158 | MetricsUtil.saveMetricsToJson( 159 | benchmarkResults, 160 | "docs/public/results/benchmark-results.json", 161 | ); 162 | console.log("JSON data saved to docs/public/results/benchmark-results.json"); 163 | }; 164 | 165 | const main = async () => { 166 | // Parse CLI arguments for handling multiple runtimes 167 | const args = process.argv.slice(2); 168 | if (args.length > 0) { 169 | const runtime = args[1].toLowerCase(); 170 | if (runtime === "bun") { 171 | // Only keep bunup entry in the bundlers array 172 | bundlers = bundlers.filter((b) => b.name === "bunup"); 173 | } 174 | } else { 175 | // Remove the bunup entry in any other runtime 176 | bundlers = bundlers.filter((b) => b.name !== "bunup"); 177 | } 178 | 179 | await benchmark(); 180 | }; 181 | 182 | main(); 183 | -------------------------------------------------------------------------------- /docs/components/Select.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 227 | 228 | 398 | --------------------------------------------------------------------------------