1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
|
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="static/Logo.svg" type="image/svg+xml">
<link rel="stylesheet" href="article.css">
<title>A solution to slow nix-shells</title>
</head>
<body>
<article class="markdown-body">
<header>
<div class="header-top">
<a href="writing.html" class="header-logo-link"></a>
<div class="header-info">
<h1>A solution to slow nix-shells</h1>
<time datetime="2026-08-26">Published Aug 8, 2026</time>
</div>
</div>
<hr>
</header>
<p>
Tired of waiting for <code><kw>nix-shell</kw></code> to re-download <code><str>github:NixOS/nixpkgs</str></code> after you <code><kw>nix-collect-garbage</kw></code> (or just randomly sometimes)?
Me too!
That's why I went down this rabit hole of making an instant nix shell.
</p>
<h2>Table of Contents</h2>
<ul>
<li><a href="#intro">What's wrong with nix-shell?</a></li>
<li><a href="#existing-solutions">Existing solutions</a></li>
<li><a href="#devshell-packages">Devshell packages</a></li>
<li><a href="#thoughts">Thoughts</a></li>
</ul>
<h2 id="intro">What's wrong with nix-shell?</h2>
<p>
First, let's clarify what I mean by a nix shell.
I'm simply referring to the idea of hiding the packages that you use for development behind a command, and managing those packages using nix.
Most people are familiar with this through the <code><kw>nix-shell</kw></code> or <code><kw>nix</kw> <prop>shell</prop></code> commands.
(For non-nix people, yes those are in fact two very different things)
</p>
<p>
My issue with how <code><kw>nix</kw> <prop>shell</prop></code> is that it has to redownload your packages if they're not in the nix store (one cause of this would be if you recently ran <code><kw>nix-collect-garbage</kw></code>), and it has to re-evaluate the correct environment variables and packages every time you launch the shell.
These two things together mean that every time you run <code><kw>nix</kw> <prop>shell</prop></code>, you'll be waiting before you can access those packages.
And, personally, I'd rather spend my time waiting for my code to compile, not waiting to have access to my compiler.
</p>
<h2 id="existing-solutions">Existing solutions</h2>
<p>
There's a lot of existing solutions to this problem, but none of them do exactly what I want.
</p>
<p>
I used to be a big fan of <a href="https://github.com/xzfc/cached-nix-shell"><code><kw>cached-nix-shell</kw></code></a>.
It's cool because it (as the name suggests) caches your nix shell, so the next time you start it up, the launch time is instant (it removes the second factor of waiting mentioned in the previous section).
But for some reason it breaks for certain packages that rely on paths that might not exist anymore.
For example, I had issues with <code><kw>rustc</kw></code> not working when launched through <code><kw>cached-nix-shell</kw></code> if it was using a cached version.
</p>
<p>
Another solution that I think too many people fall for is to just install the packages into your system <code>$PATH</code> with <code><prop>environment</prop>.<prop>systemPackages</prop></code>.
One of the great things about NixOS is that you don't have to resort to this!
On Arch, if you install a package with <code><kw>pacman</kw></code>, it has to go into your system <code>$PATH</code>.
But on NixOS, we don't have to bloat the system path with our developer dependencies.
This is <em>exactly why</em> <code><kw>nix-shell</kw></code> exists in the first place!
</p>
<p>
The most hardcore-nix-nerd option here is to manually mark your devshell as a GC root.
This tells <code><kw>nix-collect-garbage</kw></code> to not remove the path or anything that it depends on, preserving your devshells through garbage collection.
Maybe some day I'll write my own tool that wraps this functionality to provide GC-protected nix shells, but honestly that just sounds like too much work.
(for more information about how Garbage Collection works on Nix, check out <a href="https://nixos.org/guides/nix-pills/11-garbage-collector">the Nix Pill about the Garbage Collector</a>)
</p>
<p>
There's probably a million other ways that people solve this problem that I'm not familiar with.
But, I don't really care, since I already found the perfect solution for my personal use case, which I'll explain now.
</p>
<h2 id="devshell-packages">Devshell packages</h2>
<p>
Here's the solution I came up!
In your NixOS config, using home-manager or <a href="https://github.com/feel-co/hjem">hjem</a> or whatever, just build a package that launches your shell but with some extra programs.
</p>
<pre><code>{<prop>pkgs</prop>, ...}: <kw>let</kw>
<prop>rust-shell</prop> = <kw>let</kw>
<prop>rustProfile</prop> = <prop>pkgs</prop>.<prop>buildEnv</prop> {
<prop>name</prop> = <str>"rust-profile"</str>;
<prop>paths</prop> = <kw>with</kw> <prop>pkgs</prop>; [<prop>rustc</prop> <prop>cargo</prop> <prop>rust-analyzer</prop> <prop>gcc</prop>];
};
<kw>in</kw>
<prop>pkgs</prop>.<prop>writeShellScriptBin</prop> <str>"rust-shell"</str> <str>''
export PATH="${<prop>rustProfile</prop>}/bin:$PATH"
exec $SHELL
''</str>;
<prop>py-shell</prop> = <kw>let</kw>
<prop>pythonProfile</prop> = <prop>pkgs</prop>.<prop>python314</prop>.<prop>withPackages</prop> (<prop>ps</prop>:
<kw>with</kw> <prop>ps</prop>; [
<prop>numpy</prop>
<prop>matplotlib</prop>
<prop>pandas</prop>
<prop>jupyter</prop>
<prop>ipython</prop>
]);
<kw>in</kw>
<prop>pkgs</prop>.<prop>writeShellScriptBin</prop> <str>"py-shell"</str> <str>''
export PATH="${<prop>pythonProfile</prop>}/bin:$PATH"
exec $SHELL
''</str>;
<kw>in</kw> {
<prop>packages</prop> = [<prop>rust-shell</prop> <prop>py-shell</prop>];
}</code></pre>
<h2 id="thoughts">Thoughts</h2>
<p>
This approach has good parts and bad parts.
For one, it's instant. No more waiting for nix to redownload the latest version of nixpkgs and copy new paths into your system store.
And, just like nix-shell, it doesn't bloat your system <code>$PATH</code>.
The packages in the subshell are not available in the host system, just like nix-shell.
</p>
<p>
But there are some downsides as well.
</p>
<p>
The biggest one is that you can't add new packages to your devshells without rebuilding your system.
The extent to which is an issue depends on the language and how you manage libraries.
For Rust, this is not an issue.
Most people that use rust use cargo to build their software, which automatically manages dependencies per-project.
The same goes for serverside javascript with <code>node_modules</code>.
But if you prefer to use nix to manage your dependencies (like with Python, for example), this might not be the best idea.
</p>
<p>
Additionally, the versions of the packages will be tied to the version of nixpkgs that you used the last time your rebuilt your system.
This shouldn't be an issue for people like me who are constantly tweaking and rebuilding stuff.
</p>
<p>
Also, this can make your system closure really large.
In my examples, the Rust one isn't that bad, but the Python one pulls in hundreds of python dependencies into the system closure.
This can make rebuilds longer (when the packages need to update) and take up more space on your computer.
</p>
<hr>
<p>
Even with these trade-offs, I'm willing to pay the price to have instantaneous access to my developer dependencies!
</p>
</article>
</body>
</html>
|