Learn Fzf - Performance & Large Datasets
Series/Learn Fzf/Episode 17
Episode 17 of 23

Learn Fzf - Performance & Large Datasets

Optimizing fzf's speed on large datasets: choosing an algorithm with --algo, limiting items before they enter, the --sort decision, and lightening the preview load. Including modern fzf performance gains that scale linearly across CPU cores, reduced memory cache footprint, and practical strategies for millions of items.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 16 you made fzf lists come alive — periodic reloads, queries forwarded to ripgrep, and even control from external processes via --listen. All of that feels magical when the list is a thousand lines. But what happens when the list is a million lines? That's where fzf's real question begins: how fast can it filter, and how do we keep it fast.

This episode covers fzf performance from two sides. The side you can control: choosing an algorithm with --algo, limiting the number of items before they enter, the --sort decision, and lightening the preview load. The side that comes from the developers: modern fzf's ability to scale linearly across CPU cores and the drastic reduction in memory cache footprint since 0.71 — plus practical strategies for handling million-item datasets.

The first principle to understand: fzf is a filter that runs in a pipeline, and a pipeline is only as fast as its slowest component. A list that's slow to build upstream won't be saved by a super-fast fzf downstream. Performance is the responsibility of the whole chain, not just the visible end.

fzf's Performance Map

fzf's load is split into three stages that run partially in parallel:

  1. Reading input — consuming lines from stdin asynchronously.
  2. Matching — filtering lines against the query; the most CPU-hungry stage.
  3. Displaying and previewing — rendering the interface and running the preview command.

The three aren't balanced. For large lists, matching dominates; for small lists, the preview is often the slowest part. Optimizing means knowing which stage is burning your time, not just switching on every "fast" option at once.

Choosing an Algorithm: --algo

fzf provides two fuzzy matching algorithms. v2 is the default — an optimal scoring algorithm producing the best ranking quality. v1 is the old-style algorithm (the one fzy uses), simpler, with a coarser ranking quality trade-off.

Choosing a matching algorithm
fzf --algo v2
fzf --algo v1
v2 default with optimal scores; v1 is simpler

Note

--algo determines ranking quality, not just speed. v2 picks the most relevant results when many items match with close scores; v1 is easier to "misorder" on large lists. Start from v2 (default) and drop to v1 only if your measurements show that scoring isn't the bottleneck — don't lower quality without data.

Limit Items Before They Enter

The first rule of large datasets: don't send more than you need. Every line entering fzf must be stored, indexed, and potentially scored. Lines you'll never pick are just garbage slowing everything down.

The two most effective ways: limit the source (filter upstream with fd or rg, not find), and trim the size (head) when you only care about the early part:

Limit the list before it enters fzf
fd -t f . ~ | head -n 5000 | fzf
head trims the list; fd is faster than find on traversal
Only the tail of a big log
tail -f /var/log/app.log | fzf --tail 100000 --tac --no-sort --exact
--tail loads the last N lines from an endless stream

The second example uses the --tail feature: from a log stream that never ends, fzf only keeps the last 100,000 lines — the list stays bounded even though the source is endless.

The --sort Decision

Sorting is one of the most expensive operations in fzf. By default (--sort), fzf orders results by match score. When the list is huge and the order already means something — history you want to stay chronological, logs you want to stay in sequence — --no-sort removes that cost and keeps the input order as-is:

Sorting results
seq 1000000 | fzf --sort
seq 1000000 | fzf --no-sort
--sort default orders by score; --no-sort keeps the input order

For tied results under --sort, --tiebreak defines the distinguishing criterion. On large datasets, the --no-sort --exact combination is a legitimate shortcut: items are still filtered, but the heavy scoring work is skipped.

Tip

Measure before choosing. time is your friend: compare seq 1000000 | fzf --sort < /dev/null with the --no-sort version and see the difference on your own machine. Optimization without measurement is just guesswork that looks scientific.

Minimize the Preview Load

When the list is already large, what often feels slow isn't the filter but the preview. Every time the cursor moves, the preview command reruns. Three adjustments with the most impact:

  1. Use cheap commands. head and sed are far lighter than tools that parse an entire file.
  2. Turn off unneeded processing. bat with --style=plain and without --color=always avoids color-parsing work.
  3. Limit the scope. A small --preview-window and commands that only read a few lines limit work per move.
A lightweight preview
fzf --preview 'head -n 30 {}'
fzf --preview 'bat --style=plain {}'
head limits lines; plain style reduces parsing

Modern Performance: Linear Across CPU Cores

Now let's get to the developer side. Since fzf 0.71, fzf's search performance has become linearly scalable across all CPU cores: the matching work is divided among all available processor cores, so a list twice as large doesn't mean twice the time if there are cores to work on it. This differs from older versions that mostly ran on a single core.

The practical impact: on modern multicore machines, fzf filters hundreds of thousands to millions of lines with latency you no longer feel — as long as the data streams through it, instead of waiting for all the input to arrive first.

Memory Footprint: A Lighter Cache

At the same time, the 0.71 release recorded an 86x reduction in memory cache footprint per entry. That large number comes from frugal storage: the match-result cache now stores far smaller indexes than before, so lists with millions of items can fit in much more modest memory — and a smaller cache also means friendlier behavior for the CPU cache itself, speeding up repeated search iterations.

For you, this isn't just a number: it means millions of items are now reasonable to process on a laptop without making swap scream.

Strategies for Millions of Items

Summarizing everything into a runnable recipe, here are the strategies for million-item datasets:

StrategyWhyHow
Filter upstreamReduce the lines enteringUse rg/fd, not find
Keep the orderAvoid sorting cost--no-sort on already-ordered input
Skip heavy scoringReduce matching work--exact when precise matching is enough
Bound the listKeep memory reasonable--tail for streams, head for the start
Stream, don't waitDisplay before all input arrivesDon't cat file | fzf when the file can be piped live
Normalize upstreamReduce parsing inside fzfStrip ANSI, excess columns, and empty lines before entering

Important

One trap that frequently trips people up: fzf shows its interface before all input has finished reading — that's by design. But if you force synchronization with --sync, fzf waits until the input and initial search finish before drawing. On a million-line list, --sync makes the screen look "frozen" for a few seconds. Use --sync only when an initial action (like start) genuinely needs to see the complete list.

Common Mistakes

MistakeSymptomSolution
Sending millions of lines without a filterSlow from input reading onwardFilter upstream with rg/fd
--sort on already-ordered inputWasted sorting cost--no-sort keeps the input order
Expensive preview on every cursor moveFeels slow even though the filter is fasthead/sed, bat --style=plain, limit the preview
--sync on a giant listScreen "frozen" at the startDrop --sync; let fzf stream
Excess ANSI in itemsColor-parsing overheadStrip escape sequences upstream
Changing --algo without measuringQuality drops with no gainMeasure with time first, then decide

Caution

The best optimization starts from measurement: time for the pipeline, --no-sort --exact for a quick test, and observation of which preview is triggered most often. fzf 0.71 has moved much of the heavy lifting into parallelism and a frugal cache — your job isn't to race it, it's to stay out of its way.

Closing

In this episode 17 you completed fzf's performance side: choosing an algorithm with --algo (v2 default, v1 for special needs), limiting items before they enter with fd, head, and --tail, deciding --sort versus --no-sort based on what the input order means, and lightening the preview load with cheap commands and plain styles. You also understood modern fzf's improvements — search that scales linearly across CPU cores and a memory cache 86x lighter since 0.71 — along with strategies for handling millions of items in a streaming fashion.

The message to take home: fzf's speed is a collaboration between a frugal upstream and an efficient downstream. A well-bounded dataset enters a well-accelerated engine — that's the recipe.

Now you have a full toolkit for using fzf correctly. But even the best setup sometimes doesn't behave as expected — a strange prompt, results that don't appear, a silent preview. In episode 18 we cover troubleshooting & debugging: turning on --debug, understanding the internal log, and diagnosing the most common problems seen in the field.

Learn Fzf - Performance & Large Datasets | Learn Fzf