Inside Ionify — An engineering experiment with virtual compiler inputs, build artifacts, and correctness-aware invalidation.
A few days ago, I shared a Tailwind performance regression in Ionify, my open-source frontend build tool.
A developer reported that changing Tailwind classes could trigger a full rescan.
In the affected workload, a 100-file-change build increased from approximately 300ms to 1.3s.
Our first optimization reduced the measured overhead by around 40–50%.
That was encouraging.
But it raised another question:
Could we eliminate the unnecessary full rescan instead of continuing to make it progressively cheaper?
1. The First Approach: Classifying CSS Inputs
The initial direction was to distinguish between CSS inputs that could affect Tailwind's scanning requirements and inputs that only required recompilation when changed.
Conceptually:
Changed CSS input | v Classify its impact | +----+----+ | | v v Scan-relevant Other CSS | | v v Rescan Recompile affected when needed inputs This helped reduce redundant work.
But it still left us asking how much further the same strategy could be optimized.
We were reducing the cost of scanning without necessarily reconsidering the compiler's input boundary.
2. Challenging the Incremental Approach
I asked Fable, the AI coding agent assisting with the investigation, how we could move toward scanning only changed files.
Its recommendation was conservative: continue refining the existing approach, reduce scanning as much as possible, but don't promise that every change can be handled by scanning only the modified file.
That caution was reasonable.
A CSS or source change can affect other inputs through dependency relationships, shared definitions, or compiler-specific behavior.
Scanning only the modified file isn't automatically correct.
But I wanted to explore a different question.
What if we could change the input representation instead of merely reducing the number of files passed through the existing path?
3. Introducing a Virtual Compiler Input
The idea was to construct a Virtual Input from the artifacts and information required by Tailwind's compiler.
Instead of treating a large collection of physical CSS files as an inseparable set of scan inputs, Ionify could represent the required information through a controlled compiler-facing input.
Conceptually:
CSS + related inputs | v Dependency knowledge | v Required artifacts / facts | v Virtual Input | v Tailwind compiler | v Generated CSS output The interesting part isn't simply creating a virtual file.
It's being able to determine when the information represented by that virtual input has changed.
That's where Ionify's existing dependency and invalidation architecture becomes relevant.
4. Changing the Invalidation Boundary
With the virtual-input approach, the goal is to invalidate the affected style file and the relevant virtual compiler input only when their correctness-relevant dependencies change.
Conceptually:
Change detected | v Determine affected inputs | v Does required compiler information change? / \ Yes No | | v v Invalidate Preserve reusable virtual compiler knowledge input | | +----+-----+ | v Recompile affected output This is a conceptual model of the approach, not a complete description of the implementation or a guarantee that unrelated dependency changes can always be ignored.
An important distinction remains:
A file changing is not the same thing as the compiler's required information changing.
That distinction opens the possibility of reusing previously established compiler inputs even when some source files have been modified.
But every reuse decision still needs to preserve correctness.
5. The Result on Tailwind 3.4
The new approach produced an important result.
In our tested Tailwind 3.4 scenario, the unnecessary full rescan was eliminated.
That is a different kind of progress from the previous 40–50% overhead reduction.
The earlier improvement made the existing behavior less expensive.
The virtual-input approach avoided that full-rescan path in the tested case.
However, eliminating a full rescan in one scenario doesn't mean eliminating every scanning operation.
It also doesn't prove compatibility with every Tailwind feature, plugin configuration, or project structure.
Those are separate validation obligations.
6. The Correctness Problem Still Matters
A virtual input can only be trusted if it accurately represents the information required for the compiler's output.
Consider changes involving:
- Imported styles and shared definitions
- Tailwind configuration or plugin behavior
- Utility candidates discovered from source files
- Changes that alter compiler-visible directives or references
- Restored or previously cached artifacts
- Dependencies that can affect generated CSS without modifying the immediate style file
The implementation needs to establish when changes to those inputs require rebuilding the virtual representation.
Otherwise, avoiding a full rescan could produce stale or incomplete CSS.
And that wouldn't be a successful optimization.
The goal is not to invalidate as little as possible. It's to invalidate exactly what correctness requires.
7. Why This Matters Beyond Tailwind
The experiment reinforces a broader principle behind Ionify:
Performance isn't only about making operations faster. Sometimes it's about changing the representation of the work so an operation is no longer necessary.
In build systems, the input boundary and dependency model are architectural decisions.
A compiler-facing virtual input can provide a different representation of required work.
Persistent dependency knowledge can help determine when that representation needs to change.
And explicit invalidation contracts can define when previous results remain reusable.
The challenge is keeping those responsibilities consistent rather than introducing an independent optimization that becomes another source of truth.
8. What's Next?
The result on Tailwind 3.4 is encouraging, but the investigation is ongoing.
The next objective is broader Tailwind 3 coverage, including scenarios where compiler inputs, dependencies, and generated CSS interact in less obvious ways.
I'll be focusing on validating that the optimization preserves expected output while avoiding unnecessary rescans.
I'm sharing this before calling the work complete because the original regression came from a developer report, and feedback has been valuable throughout the investigation.
Final Thought
The first question was:
How do we make Tailwind scan fewer files?
The more interesting question became:
Can we represent the compiler's required inputs differently so the unnecessary scan doesn't happen at all?
That change in perspective led to a promising result.
And it's exactly the kind of architectural experimentation I'm exploring with Ionify.
Ionify is open source:
https://github.com/ionifyjs/ionify
If you've worked with Tailwind internals, compiler input models, or incremental build invalidation, I'd love your feedback.
What edge case would you test first to challenge the correctness of a virtual-input approach?

