This stems from a mismatch of priorities: Nix cares more about perfect reproducibility of your stack than in-dev performance. In cases where you care about compiler caches (or simply object files in the C world) nix currently is simply the wrong tool for the job.
The path that people usually take is to use ecosystem-specific tooling (e.g. go) during development, with nix devshells to serve system library dependencies, and to then use nix exclusively as a system integration/CI build tool to then build the production artifacts. This gives you compiler artifacts when you need them (i.e. when you recompile and change little things until it stops throwing errors), and reproducibility when you need it (when you want to publish a binary and be certain you’re not throwing some random developer laptop’s malware at your customers).
IMO the underlying problem is that our industry keeps reinventing the build system wheel for every language under the sun. Each language has subtly different build mechanisms, and they all don’t integrate very well, but because the tooling exists and is standard for the respective languages it’s almost impossible to substitute other tooling, especially if you care about keeping up with the ecosystem standard (to make it easier for new devs who interact with your dev stack).
Without committing the gargantuan effort of perfectly reproducing each respective ecosystem’s build system in a transparent way that still gives you all the nix advantages (which is often impossible, since the ecosystem-specific build systems are usually not designed with reproducibility in mind), you can’t really have your cake and eat it too. Nix’ ecosystem-adapted build tooling lives in the twilight zone, where things are just convenient enough that you can build, but not so high-effort that keeping up with upstream changes becomes infeasible (especially given the relatively small number of nix ecosystem developers).
As such, the true solution to this problem is to work towards a real standard for this process cross-ecosystem. At the end of the day all we’re all doing is taking some input files and producing some output files, with some intermediate results which we’d like to keep track of for work avoidance; this shouldn’t be so complex. Begin by standardizing dependency handling so we don’t have two dozen different “crate”/“project”/etc. “registries”…
With that said, the idea of exposing artifact caches comes up every once in a while, but it’s heavily resisted since it’d break reproducibility guarantees. Doing that is quite bad even if you don’t care about reproducibility at all, because now the entire cache model becomes problematic; if a faulty compiler cache on some build machine can cause cache poisoning, you need way more tooling and UI/UX to handle those edge cases than nix currently has.
It’s also not as trivial as you’d hope; all the various languages have different expectations for how their build artifacts are persisted. You’d need lots of language-specific handling to make this work. Often atime/mtime matters, nix builds are quite allergic to that.
All that out of the way, BuildStream (a project I was part of what feels like half a lifetime ago) has an interesting feature that lets you pierce this exact veil during development; it has a concept of a “workspace” that is effectively a checkout of a project in your system tree. It allows developing/changing files in it, which it will then reuse during the build. This can give you incremental builds.
Give it a shot, it’ll show you all the downsides of doing this pretty quickly. Other than that it’s a perfectly cromulent build tool IMO, just falls short on performance (at least back when I still worked with it, projects written in python have their limitations) and lacks the ecosystem adoption nix has (as well as the turing-complete config language, while I agreed that that was a reasonable idea at the time, nix has convinced me that that was a mistake - all the other tools use imperative languages, a functional language definitely seems like the right call).