ブログ一覧

Remix 3 を Next.js と比べたら、DB 同梱は本当で圧縮は自分で入れる

更新: Web開発者向け

2026-10-02 に正式版が出た Remix 3(Remix 3.0.0 のリリースノート) と Next.js 16 で同じ問い合わせフォームを作って比べました。

  • 本当だった: DB、マイグレーション、入力の検証が入っていて何も足さずに使える、DB を読むページが 2.5〜3倍の要求をさばける
  • 自分で入れる物: 圧縮。既定では切れていて、1行足すとブラウザに送る JS は Next.js の約 3分の1 になる。ただし圧縮の強さを下げないと、要求のたびに描くページが 1秒にさばける数が Next.js を下回る

Remix 3 は React を使わずに Web の標準(Request・Response・FormData)の上に作られ、ルーター、UI、フォームの検証、DB とマイグレーション、テストまでを remix という 1つのパッケージに入れた Web フレームワークです(Remix 3 の RC の告知)。

Remix 3 は SSR が前提で、動かすにはサーバーが要ります。Next.js のように、ビルド時に HTML を書き出して静的なホスティングに置く使い方(SSG や static export)は、同梱の文書には見当たりませんでした。

作ったのは次の 3画面です。

  • GET /contact のフォーム(名前 1〜40字、メールの形式、本文 1〜1000字)
  • POST /contact で入力を検証し、不正なら項目ごとのエラーと入力を残して返し、正しければ SQLite に保存してお礼のページへ
  • GET /admin/inquiries で一覧(新しい順)

雛形は npx remix@3.0.0 new と npx create-next-app@latest(Next.js 16.3.8、App Router、TypeScript)で、同じ仕様のアプリをそれぞれ 3つずつ作り、Node 24.21、4コア・メモリ 15GB の同じ機械で本番の起動にして測りました。数字はすべて 2026-10-06 に取りました。

依存を足さずに使えた DB・マイグレーション・入力の検証

雛形の dependencies は Remix 3 が remix 1つ、Next.js が next・react・react-dom の 3つです。DB を使うフォームを作っても、Remix 3 は remix 1つのままです。

Remix 3                      : {"remix":"^3.0.0"}
Next.js(better-sqlite3 を使う): {"better-sqlite3":"^13.0.3","next":"16.3.8","react":"19.2.8","react-dom":"19.2.8"}

Remix 3 では DB を remix/data-table で書け、マイグレーションは db/migrations/0001_create_inquiries/up.sql と down.sql を置いて、起動時に次のコードで当てるだけでした。

import { loadMigrations } from 'remix/data-table/migrations/node'
import { createSqliteDatabase } from 'remix/data-table/sqlite'

export const db = createSqliteDatabase({ filename: 'data/app.db', foreignKeys: true })

export async function migrateDatabase() {
  await db.migrate(await loadMigrations('db/migrations/'))
}

入力の検証も remix/data-schema に入っています。Zod と同じ Standard Schema に沿った書き方で、FormData をそのまま検証にかけられます。

import * as s from 'remix/data-schema'
import * as checks from 'remix/data-schema/checks'
import * as f from 'remix/data-schema/form-data'

export const contactForm = f.object({
  name: f.field(s.string().pipe(checks.minLength(1), checks.maxLength(40))),
  email: f.field(s.string().pipe(checks.email())),
  message: f.field(s.string().pipe(checks.minLength(1), checks.maxLength(1000))),
})

// アクションの中で
let parsed = s.parseSafe(contactForm, context.formData)

Next.js には DB と検証の仕組みが無いので、Server Actions の中で検証するにも DB に書くにも、ライブラリを選んで足します。検証なら Zod など、DB なら better-sqlite3(node_modules が 26MB 増える)や Node の組み込みの node:sqlite、マイグレーションは Prisma や Drizzle のような ORM の物を使うか自前で書きます。

node_modules は TypeScript を含めて Remix 3 が 89MB、Next.js が 358MB でした。ただし入るパッケージの数は Remix 3 が 95、Next.js が 29 です。remix の中身が @remix-run/ の小さなパッケージに分かれているためで、「依存 1つ」は直接の依存の話です。

DB を読むページは 2.5〜3倍、静的なページは Next.js が速い

Remix 3 には本番用のビルドの手順がありません。雛形の npm run start は node --import remix/node-tsx server.ts で、TypeScript のソースを起動のときにそのまま読み込みます。Next.js は next build で変換と束ねを済ませてから next start で動かすので、デプロイの手順が 1つ多くなります(この小さいアプリでビルドは約 4秒)。起動してから最初の要求に答えるまでは Remix 3 が 0.7秒、Next.js が 0.5秒で、大きな差はありませんでした。

問い合わせを 21件入れた一覧のページ(DB を読んで描く)に、同じ機械で 10本同時に 10秒間 autocannon で要求を送った結果は次のとおりです。範囲は 3つずつのアプリで測った幅です。Next.js の一覧は App Router の普通のページ(サーバーで毎回描く React の部品)で、差がフレームワークのどこから来るかまでは切り分けていません。

  • 一覧のページ(1秒あたりの件数): Remix 3 が 744〜894件 / Next.js が 291〜297件
  • 中央値の遅延: Remix 3 が 10〜12ミリ秒 / Next.js が 30〜31ミリ秒
  • 雛形のトップ(静的なページ、1秒あたりの件数): Remix 3 が 311件 / Next.js が 1,721件
  • 起動後のメモリ(RSS): Remix 3 が約 325MB / Next.js が約 420MB

同じ条件で一覧のページを取り直した 1回分の出力です(上が Remix 3、下が Next.js、表の見出しと遅延・1秒あたりの件数の行を抜粋)。平均は 698件と 278件で上の範囲より少し低く出ましたが、比は 2.5倍で同じでした。

Running 10s test @ http://localhost:4101/admin/inquiries
│ Stat    │ 2.5%  │ 50%   │ 97.5% │ 99%   │ Avg      │ Stdev   │ Max   │
│ Latency │ 10 ms │ 12 ms │ 28 ms │ 46 ms │ 13.82 ms │ 5.68 ms │ 82 ms │
│ Req/Sec   │ 451     │ 451     │ 728     │ 837     │ 697.8   │ 113.54  │ 451     │
7k requests in 10.02s, 71.4 MB read

Running 10s test @ http://localhost:4201/admin/inquiries
│ Stat    │ 2.5%  │ 50%   │ 97.5% │ 99%   │ Avg      │ Stdev    │ Max    │
│ Latency │ 27 ms │ 30 ms │ 83 ms │ 91 ms │ 35.39 ms │ 14.39 ms │ 124 ms │
│ Req/Sec   │ 140     │ 140     │ 300     │ 339     │ 278.11  │ 67.14   │ 140     │
3k requests in 10.02s, 54.2 MB read

雛形のトップは next build の出力で「○ (Static)」と出る、ビルド時に描いて置いておくページです。Remix 3 はこのページも毎回描き、しかも雛形のトップは 31KB と一覧より大きな案内のページなので、ここでは Next.js が速くなりました。Remix 3 のトップが一覧より遅い理由までは調べていません。どちらもこの小さいアプリでの数字です。

圧縮は自分で入れ、強さを下げる

Remix 3 の雛形は、HTML も JS も圧縮せずに返します。一覧のページでブラウザに送る JS は、中身が Remix 3 は 47本で約 118KB、Next.js は 5本で約 442KB と、Remix 3 のほうがずっと小さいです。それでも Next.js は gzip で約 130KB に縮めて送るので、転送量はほぼ同じでした。

$ curl -sI -H 'Accept-Encoding: gzip, br' localhost:4101/assets/app/actions/public/entry.ts   # Remix 3(雛形のまま)
content-type: application/javascript; charset=utf-8
$ curl -sI -H 'Accept-Encoding: gzip, br' localhost:4201/_next/static/chunks/2j-dcpuz6akpa.js   # Next.js
Content-Type: application/javascript; charset=UTF-8
Content-Encoding: gzip

圧縮のミドルウェアは remix に入っていて、ルーターに 1行足せば効きます。使わない理由は文書にも書かれておらず、雛形に入っていないだけです。

import { compression } from 'remix/middleware/compression'

export const router = createRouter({
  middleware: [compression(), /* 雛形のミドルウェア */],
})

足すと JS の転送量は約 41KB(Brotli)、gzip でも約 45KB になり、Next.js の約 3分の1 でした。

圧縮には 2つの面があります。見る人の側では、届く量が減るので、回線が遅いほどページは速く表示されます。サーバーの側では、返すたびに圧縮の計算をするので、1台のサーバーが大勢からの要求をさばける数が減ります。

既定の compression() は Brotli を Node の既定の最高の強さ(11)で使うので、この計算が重くなります。前の節と同じ一覧のページに 10本同時に要求を送り、1秒にさばけた数を比べました。強さを 4 に下げると、届く JS は約 46KB とほとんど変わらず、さばける数は Next.js を上回りました。

  • 圧縮なし: 750件前後
  • 既定の `compression()`: 147件
  • Brotli の強さを 4 に下げた場合: 351〜573件(多くは 390件前後)
  • Next.js(gzip): 231〜254件
import zlib from 'node:zlib'

compression({ brotli: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4 } } })

前に CDN やリバースプロキシを置いて、そちらで圧縮する構成なら、Remix 3 の側では入れなくても足ります。

47本に分かれるのは、Remix 3 がビルドで束ねないためです。内訳は UI の部品(@remix-run/component)が 38本、import map の補い(polyfill)が 7本、アプリの入口が 1本などで、操作の無い一覧のページでも部品がファイルごとに届きます。

試していない所

  1. 大きいアプリ(認証、複数の表の関係)や、長く変更を重ねた場合
  2. サーバーと UI のフルスタックの HMR、SPA、UI 部品(@remix-run/ui)、Cloudflare Workers のような Node 以外の実行環境
  3. 既存の Next.js のアプリから移す作業(Remix 3 は Node 24.3 以上が必要)

この記事をシェア