Speeding up slow lint-staged runs Jump to heading
A pre-commit hook that takes twenty seconds will be bypassed. Not maliciously β people commit with --no-verify βjust this onceβ when they are in a hurry, then again, and soon the hook protects nothing. lint-staged is supposed to be fast because it processes only staged files, but real configurations accumulate slow parts: a linter loading a large rule set, a type check over the whole project, a tool started once per file, caches disabled. This page shows how to measure where the time goes and cut it down without dropping the checks that matter, within lint-staged formatting automation.
When to use this approach Jump to heading
- The pre-commit hook takes more than a few seconds on a typical small commit.
- People routinely commit with
--no-verify, and the reason is speed. - The hook got slower after adding a tool or growing the repository.
- You have added type checks or other project-wide steps, as in running type checks alongside lint-staged.
Step 1 β Measure before changing anything Jump to heading
Time the whole hook on a realistic commit, then each task. --debug prints task timings; running each command by hand on the same files confirms them.
git add src/billing/totals.ts src/billing/totals.test.ts
time npx lint-staged --debug 2>&1 | grep -E 'Running tasks|finished|took'
# Time individual tools on the same files
time npx eslint src/billing/totals.ts src/billing/totals.test.ts
time npx prettier --check src/billing/totals.ts src/billing/totals.test.ts Step 2 β Turn on every toolβs cache Jump to heading
Most linters can cache results per file, keyed on content and configuration. ESLint does not cache by default; turning it on often halves its time on repeat runs.
// .lintstagedrc.mjs
export default {
"*.{ts,tsx,js}": [
"eslint --cache --cache-location node_modules/.cache/eslint/ --fix",
"prettier --write --cache",
],
"*.py": ["ruff check --fix", "ruff format"], // ruff caches by default
}; # Verification: second run on the same files is much faster
time npx eslint --cache --cache-location node_modules/.cache/eslint/ src/billing/totals.ts
time npx eslint --cache --cache-location node_modules/.cache/eslint/ src/billing/totals.ts Keep cache directories inside node_modules/.cache or another ignored path so they never get committed.
Step 3 β Start each tool once, not once per file Jump to heading
lint-staged passes all matching files to a command in one invocation, but some configurations defeat that β a shell loop, a wrapper script that processes one file at a time, or a function returning a command per file. Process start-up dominates for tools like ESLint that load many plugins.
// Slow: one ESLint process per file
"*.ts": (files) => files.map((f) => `eslint --fix ${f}`),
// Fast: one ESLint process for all staged files
"*.ts": "eslint --cache --fix", Step 4 β Narrow globs and move heavy checks out Jump to heading
Globs that match more than necessary waste time: * runs a tool on images and lockfiles it will ignore anyway. And checks whose cost does not depend on the number of staged files β full type checks, test suites β belong in pre-push or CI.
export default {
"*.{ts,tsx}": ["eslint --cache --fix", "prettier --write --cache"],
"*.{json,md,yml}": "prettier --write --cache",
// removed: "*": "prettier --write" β matched binaries and lockfiles
// moved to .husky/pre-push: "tsc --noEmit"
}; Step 5 β Keep it fast as the repository grows Jump to heading
Speed regressions creep in as tools are added. Record the hookβs time in a cheap way and look at it occasionally.
# .husky/pre-commit β log duration locally, never block on it
start=$(date +%s)
npx lint-staged
status=$?
echo "$(date +%F) $(( $(date +%s) - start ))s" >> "$(git rev-parse --git-dir)/hook-timings.log"
exit $status # How has it trended?
tail -20 "$(git rev-parse --git-dir)/hook-timings.log" | awk '{s+=$2; n++} END {print "avg:", s/n "s"}' If the average creeps past five seconds, repeat step 1. The goal is a hook nobody has a reason to skip.
Step 6 β Warm caches after switching branches Jump to heading
Caches keyed on file content survive branch switches, but some tools invalidate their whole cache when configuration changes β and switching between branches with different lint configuration does exactly that. The first commit after a switch is then slow again. A post-checkout hook can warm the cache in the background so the next commit is fast.
# .husky/post-checkout β refresh lint caches in the background after a branch switch
[ "$3" = "1" ] || exit 0 # only for branch checkouts, not file checkouts
( npx eslint --cache --cache-location node_modules/.cache/eslint/ src/ >/dev/null 2>&1 & ) Background work in hooks must never block or fail the operation that triggered it, which is why the command runs detached and its output is discarded. If warming the cache takes longer than the time to the next commit, the commit simply uses whatever is cached by then.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Is it worth running tasks in parallel? Jump to heading
lint-staged already runs different globsβ tasks concurrently. Within a glob, tasks run in sequence because a formatter and a linter on the same file must not race. Parallelism inside a tool, such as ESLintβs own concurrency options, can help on large commits.
Should the hook ever run tests? Jump to heading
Only very fast, targeted ones β tests for the staged files, finishing in a second or two. Anything broader belongs in pre-push, as in running a fast test subset in a pre-push hook.
What about huge commits, such as a mass rename? Jump to heading
Large staged sets will be slow whatever you do. lint-staged can split long file lists into chunks; for one-off mass changes, committing with --no-verify and relying on CI is reasonable, as long as it is the exception.
Related Jump to heading
- Lint-Staged Formatting Automation β the parent topic.
- Debugging lint-staged Failures β when the hook is broken rather than slow.
- Disabling Git Hooks Temporarily Without Breaking the Team β the controlled alternative to --no-verify.
- Caching pre-commit Environments in CI β the same caching mindset for the pre-commit framework.