# 06 — The classpath-scan tax, measured ← prev [05 — JFR instead of a buffer](05-jfr-instead-of-a-buffer.md) · next → [07 — Failure modes](07-failure-modes.md) --- "Narrow your component scan" is standard advice with no number attached to it. Here is the number, from [`docs/output/04-scan-tax.txt`](output/04-scan-tax.txt). The experiment adds classes to the package `@SpringBootApplication` already scans and changes nothing else. `plain` classes carry no annotation at all, which separates the cost of *scanning* from the cost of *creating beans*. ``` variant | Started in (s) | parse ms | instMs | steps ---------------------------+---------------------+-----------+-----------+------- baseline | 7.293,6.685,6.86 | 1055.69 | 4854.73 | 400 +5000 plain classes | 7.626,7.143,7.04 | 1617.38 | 4468.31 | 400 +5000 @Component | 11.02,11.647,11.481 | 3221.82 | 6097.98 | 5400 +5000 @Component, lazy | 9.688,9.374,9.609 | 3434.76 | 4982.61 | 324 ``` Reproduce with `RUNS=3 ./scripts/demo-scan-tax.sh` (about ten minutes — each variant is a full rebuild). ## What the rows say **5,000 classes that are not beans cost ~560 ms.** `parse` goes from 1056 ms to 1617 ms while the step count stays at exactly 400 and instantiation is unchanged. That is roughly **0.11 ms per class inspected** on this machine. The scanner opens every `.class` file under the base package and reads its annotation metadata before it can decide the class is uninteresting. Classes you never wrote a `@Component` on are still on the bill. **Making them beans roughly triples the tax.** `parse` goes to 3.2 s, instantiation gains 1.2 s, and the timeline goes from 400 steps to 5,400. Startup goes from ~6.9 s to ~11.5 s. **Lazy initialisation does not touch the scan.** With the same 5,000 components, `spring.main.lazy-initialization=true` leaves `parse` at 3.4 s — statistically unchanged — and buys back 1.1 s of instantiation, for ~9.6 s. Lazy init defers *construction*. Bean definitions are still created, and every class is still scanned, because Spring cannot know whether a class is a bean without looking at it. That is the important asymmetry: **scanning is paid at startup no matter what you do at runtime.** The only fix is to scan less. Note also that the lazy row records **324 steps against a baseline of 400**. Lazy initialisation makes the startup tree *less* informative at exactly the moment you are trying to read it — the beans it defers never produce a `spring.beans.instantiate` step, so the cost moves to the first request and out of your recording entirely. ## What to do about it - Set `scanBasePackages` explicitly rather than relying on the package of the main class. An application whose main class sits in `com.example` scans `com.example.**`, which on a large monolith is everything. - Watch the `classCount` tag on `spring.context.config-classes.parse` (visible under JFR, see [chapter 05](05-jfr-instead-of-a-buffer.md)). This application reports 130. If yours reports several hundred, most of them arrived with a starter you are not using. - `spring-context-indexer` still ships (7.0.9 is on Central) and writes a `META-INF/spring.components` index at compile time so the scanner does not walk the classpath. It is worth knowing about; Spring's own direction of travel is AOT instead. - The AOT cache is the bigger lever, and it is measured in [chapter 08](08-what-actually-helps.md). --- ← prev [05 — JFR instead of a buffer](05-jfr-instead-of-a-buffer.md) · next → [07 — Failure modes](07-failure-modes.md)