はじめに
今年もアドベントカレンダーの時期がやってまいりました
気になっていたけど全く触っていなかったBiomeを触ります
「処理が早い」を一番よく聞くので、実際どの程度違うのか簡単に計測します
とりあえず一番Web業界で使われていそうなNext.jsのアプリケーションで試します
流れ
- 簡単なアプリケーションをNextJSで作成
- ダミーコードを大量に生成
- ESLint & Prettierでlintして実行時間を計測
- Biomeを導入して実行時間を計測
- 比較
環境
- OS: Windows11
- Terminal: Git Bash
今回作ったものは下記にpushしておきました
https://github.com/engabesi/test-biome
検証
アプリケーションの作成
まずはベースとなるアプリケーションを作成します
設定値はデフォルト
$ npx create-next-app@latest test-biome --typescript --tailwind --eslint
Need to install the following packages:
create-next-app@16.1.0
Ok to proceed? (y) Y
√ Would you like to use React Compiler? ... No / Yes
√ Would you like your code inside a `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to customize the import alias (`@/*` by default)? ... No / Yes
...
Success!
Googleさんが気を利かせて翻訳してくるのでjaに変更
return (
- <html lang="en">
+ <html lang="ja">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
機能はシンプルに、ボタンを押すとカウントアップするだけのものに
'use client';
import { useState } from 'react';
export default function Home() {
const [count, setCount] = useState(0);
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<div className="text-4xl mb-4">Count: {count}</div>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={() => setCount(count + 1)}
>
Increment
</button>
</main>
);
}
ESLint & Prettier構成にする
現在デファクトスタンダードである ESLint + Prettier 環境を整える
npm install --save-dev prettier eslint-config-prettier
import nextTs from "eslint-config-next/typescript";
+import prettierConfig from "eslint-config-prettier";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
+ prettierConfig,
"start": "next start",
- "lint": "eslint"
+ "lint": "eslint",
+ "format": "prettier --write .",
+ "lint:format": "npm run lint && npm run format"
},
"dependencies": {
ESLintとPrettierを動かします
$ time npm run lint:format
> test-biome@0.1.0 lint:format
> npm run lint && npm run format
> test-biome@0.1.0 lint
> eslint
> test-biome@0.1.0 format
> prettier --write .
app/globals.css 22ms (unchanged)
app/layout.tsx 44ms (unchanged)
app/page.tsx 6ms
eslint.config.mjs 14ms (unchanged)
next.config.ts 3ms (unchanged)
package-lock.json 61ms (unchanged)
package.json 1ms (unchanged)
postcss.config.mjs 2ms (unchanged)
README.md 22ms (unchanged)
tsconfig.json 2ms (unchanged)
real 0m4.146s
user 0m0.061s
sys 0m0.000s
大体4秒程度
初期でも結構かかるなという印象
また、以後共通の手順として、lintを走らせたあとの差分はすべて戻します
アプリケーションの肥大化
Lintの実行時間を長くするためにダミーファイルを作ります
手でやるのは面倒なのでスクリプトを作成し実行
スクリプトはCopilot君が頑張ってくれました
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('fs');
/* eslint-disable @typescript-eslint/no-require-imports */
const path = require('path');
const DIR = path.join(__dirname, 'app', 'dummy');
if (!fs.existsSync(DIR)) fs.mkdirSync(DIR);
// 複雑な計算をするコンポーネントを500個生成
for (let i = 0; i < 500; i++) {
const content = `
import React, { useState, useEffect, useMemo, useCallback } from 'react';
interface Props${i} {
${Array.from({ length: 50 }, (_, j) => `prop${j}: string;`).join('\n ')}
}
export const DummyComponent${i} = (props: Props${i}) => {
// AST解析に負荷をかけるための無駄なオブジェクト定義
const heavyObject = {
${Array.from({ length: 500 }, (_, j) => `prop${j}: "value${j}",`).join('\n ')}
};
// Hooks の大量使用でESLintの負荷を増やす
const [state${i}, setState${i}] = useState(0);
${Array.from({ length: 10 }, (_, j) => `const [state${i}_${j}, setState${i}_${j}] = useState(${j});`).join('\n ')}
// useEffect で dependency チェックの負荷を増やす
useEffect(() => {
console.log('mounted');
}, [state${i}]);
// useMemo で複雑な依存関係
const memoValue = useMemo(() => {
return Object.keys(heavyObject).reduce((acc, key) => acc + key.length, 0);
}, [${Array.from({ length: 10 }, (_, j) => `state${i}_${j}`).join(', ')}]);
// useCallback でさらに負荷
const handleClick = useCallback(() => {
setState${i}(prev => prev + 1);
}, []);
// 深いネストと複雑な条件分岐
const renderContent = () => {
if (state${i} > 100) {
return ${Array.from({ length: 20 }, (_, j) => `
state${i}_${j % 10} > ${j} ? <div key="${j}">Content ${j}</div> :`).join('')}
<div>Default</div>;
}
return null;
};
return (
<div onClick={handleClick}>
<h1>Dummy ${i}</h1>
<p>State: {state${i}}</p>
<p>Memo: {memoValue}</p>
{Object.keys(heavyObject).map(key => (
<span key={key} style={{ color: key.length % 2 === 0 ? 'red' : 'blue' }}>
{key}
</span>
))}
${Array.from({ length: 50 }, (_, j) => `<div key="div${j}">{props.prop${j}}</div>`).join('\n ')}
{renderContent()}
</div>
);
};
`;
fs.writeFileSync(path.join(DIR, `Dummy${i}.tsx`), content);
}
console.log('500個のダミーコンポーネントを生成しました。');
$ node create-dummy.js
500個のダミーコンポーネントを生成しました。
計測:ESLint + Prettier
肥大化した状態で、ESLintの実行時間を計測します
$ time npm run lint:format
> test-biome@0.1.0 lint:format
> npm run lint && npm run format
> test-biome@0.1.0 lint
> eslint
> test-biome@0.1.0 format
> prettier --write .
...
real 0m57.757s
user 0m0.030s
sys 0m0.030s
とんでもなく長くなりました
Biome導入
これをBiomeにするとどれぐらい変わるのか確認します
$ npm install --save-dev --save-exact @biomejs/biome
$ npx @biomejs/biome init
"lint": "eslint",
"format": "prettier --write .",
- "lint:format": "npm run lint && npm run format"
+ "lint:format": "npm run lint && npm run format",
+ "biome": "biome check --write ."
},
"dependencies": {
"next": "16.1.0",
計測: Biome
同じ500個のダミーファイルがある状態でBiomeを実行します
$ time npm run biome
...
Found 2504 errors.
Found 5500 warnings.
Found 2 infos.
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
× Some errors were emitted while applying fixes.
real 0m5.170s
user 0m0.015s
sys 0m0.045s
まとめ
| ツール | 実行時間(real) |
|---|---|
| ESLint + Prettier | 57.8s |
| Biome | 5.2s |
10倍早く終わりました かなり速度差がありますね
Biomeが早いとは聞いてましたがここまで顕著にでるとは思っていませんでした
実際の現場では乗り換えることは足りない部分もあり中々難しいと思いますが、
成熟してなお状況が変わらなければ、Biomeはかなり強力な選択肢になると感じました
とりあえず個人でさっと作るものにはBiome使っていこうかと思います
