Skip to content
shellhacks.dev
Performance

When Optimizing a Script Is Not Worth It and Where It Bites

When optimizing a script is not worth it: measure first, the cost of clever code, where speed matters, premature optimization traps and a decision rule.

6 min read
Summarize in AI
Illustration for "When Optimizing a Script Is Not Worth It and Where It Bites": a shell script in an editor, progress bars, a loop arrow.

Performance advice, including most of this cluster, makes it easy to start tuning everything. The discipline is deciding when not to. Speed work has a price in time, readability and new bugs, and it pays back only when the saved time is used by someone. The idea is old: the problem is optimizing the wrong thing at the wrong time. This post gives a short test, and it complements profiling a script, the cost of fork and exec and trustworthy benchmarks.

The arithmetic of benefit

Text
saved per run     x   runs per day   x   people/jobs affected   =   time recovered
 90 s faster          1 run/day             1 cron job              90 s a day

Cost of the change: 3 hours to rewrite, test and review, plus future confusion.
Break-even: 120 days, if nothing breaks.

Writing the numbers down is most of the decision. A nightly job that takes two minutes does not need to take 30 seconds if nobody waits for it. A script that developers run 50 times a day and that takes 20 seconds does, because the saved time repeats for every developer. The figures are an illustration; use yours. Be wary when the benefit is "it feels slow" without a count of how often.

Measure before touching anything

Shell
time ./report.sh
PS4='+ $EPOCHREALTIME ${LINENO}: ' bash -x ./report.sh 2>&1 | awk '{ if (prev) printf "%.3f %s\n", $2-prev, last; prev=$2; last=$0 }' | sort -rn | head -5
Text
real    0m42.1s
31.884 +++ 1738867501.114 41: curl -s https://api.example.test/export
 6.410 +++ 1738867533.001 58: sort big.tmp

The measured time shows that 32 of 42 seconds are one network call, so rewriting the loops would gain nothing. This is Amdahl's law in practice: speeding up a part that accounts for 5 percent of the time can never save more than 5 percent. Profile first, as in finding the slow part of a pipeline, and fix the largest piece. The sample output is illustrative.

What optimization costs

Shell
# clear
total=0
while read -r n; do total=$((total + n)); done < numbers.txt

# fast, terse, and harder to maintain
awk '{s+=$1} END{print s}' numbers.txt

Sometimes the fast version is also clearer, as here, and then there is no trade-off. More often, speed comes from tricks: file descriptors reused across calls, parallelism, caching with invalidation rules, one-liners that fuse three steps. Each adds failure modes: race conditions, stale data, quoting traps. If the optimised version is wrong in an edge case, its speed is worthless. Keep the original as a reference and compare outputs, as described in benchmarking properly.

When it is worth it

Text
Worth optimizing                              Usually not worth it
loop over 100,000 items spawning processes    a one-time migration
tool run many times per day by many people    a nightly job nobody waits for
startup time of a shell prompt or hook         a script dominated by network latency
CI pipeline minutes billed per run            a ten-line helper

Hot paths (the prompt that runs on every command, git hooks, completion scripts) benefit from careful work, since small savings repeat thousands of times. CI time is directly billed and waited on. In these cases, the simple techniques in this cluster, such as batching and removing subprocesses, are low-risk first steps before anything exotic. See speeding up shell startup and speeding up CI.

A decision rule

Ask in order: does anyone wait for this? Is it known where the time goes? How much would the best case save, and how often? Is there a design change (avoid the work, cache, batch) that beats micro-tuning? If the answer to the first is no, stop. If you proceed, change one thing, measure again and keep the change only if the gain is clear. Write down what you changed and why, so the next person does not undo it as "unnecessary complexity".

The hidden costs of faster code

Optimised code is rarely free to maintain. It tends to be denser, to rely on subtle assumptions and to have less obvious failure modes. A cache turns a simple function into one that can return stale answers, and every cache needs an invalidation rule that someone must remember. Parallelism turns a sequence into a schedule and brings races, ordering and resource limits with it. A clever one-liner saves lines and costs the next reader several minutes of decoding. None of these is a reason to avoid the techniques, but each should be paid for by a measured benefit.

Optimisation also changes what you can verify. A simple version can be checked by reading it. A tuned version needs tests that cover the paths the tuning created, and the tests must continue to compare its output with the simple version's on realistic input. Keep the plain implementation in the repository, perhaps as a test oracle, so that any later change to the fast one can be compared against something known to be right.

Small scripts, big effects

The places where a small saving matters are those that run on every action: shell prompts, completion functions, git hooks and editor save hooks. A prompt that takes 200 ms to render makes the terminal feel sluggish every time it appears; trimming it to 20 ms makes a visible difference to the person who sees it a thousand times a day. Look there first. For batch work, the benefit scales with data volume, so measure with realistic size, not with the toy file used during development. A loop that is instant on ten lines and takes an hour on ten million is an algorithm problem, and it deserves attention before it becomes an incident.

Making a deliberate decision

Record the decision, either way. If you decide to leave a slow script alone, write a comment with the measurement and the reason: "takes 90 s, runs nightly, nobody waits". It stops the next person from starting the same analysis and tells them what would change the answer, such as a higher run frequency. If you decide to optimise, state the goal in numbers before starting, for example "under 10 s on the standard input", and stop when you reach it. Without a target, tuning continues until the code is unreadable.

Spend the effort where it compounds

A lasting improvement often comes from removing the work. Skipping a step whose result is unchanged, processing only new data, or fetching less beats making the same work faster. Check whether the output is needed at all, whether it needs to be that fresh, and whether the input can be narrowed upstream. These changes are usually simple to make and simple to understand, and they compound, because less work also means fewer failures and a smaller bill.

A useful habit is to keep a short list of performance facts for each script that matters: the typical runtime, the input size it was measured on and the slowest step. When someone reports that the script feels slow, you compare against the list, not against memory. Without it, every complaint starts a new investigation, and with it, many complaints end in one comparison and a single sentence.

Where this does not apply

This is not an argument for ignoring performance: algorithmic problems (a quadratic loop over a growing input) become outages, and design decisions are expensive to change later. The advice is to spend effort where measurement points. Systems with strict latency or cost limits are a different matter and deserve earlier performance work. See Donald Knuth's often-misquoted passage in Structured Programming with go to Statements, and the performance methodology pages by Brendan Gregg.

Taggedscripting
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.