Recently, I was wondering why my computer was not starting way faster than it does. It’s a lot more powerful than the previous one, yet the difference does not feel very big.

As often, it turns out it’s due to IO waiting.

1. Let’s ask!

NixOS uses systemd, so we can directly ask systemd to nicely show what takes time:

➤ systemd-analyze critical-chain
The time when unit became active or started is printed after the "@" character.
The time the unit took to start is printed after the "+" character.

graphical.target @12.150s
└─multi-user.target @12.150s
 └─network-online.target @12.149s
  └─dhcpcd.service @1.270s +10.879s
   └─basic.target @1.257s
    └─sockets.target @1.255s
     └─pcscd.socket @1.254s
      └─sysinit.target @1.243s
       └─systemd-timesyncd.service @1.214s +27ms
        └─systemd-tmpfiles-setup.service @1.204s +6ms
         └─local-fs.target @1.202s
          └─run-user-1000-gvfs.mount @8.610s
           └─run-user-1000.mount @7.394s
            └─local-fs-pre.target @225ms
             └─systemd-remount-fs.service @193ms +30ms
              └─systemd-journald.socket @172ms
               └─-.mount @163ms
                └─-.slice @163ms

First thing to notice is the jump from @225ms to @7.394s: this is the step where I’m being asked for my password to mound the partition (run-user-1000.mount).

The next big jump is +10.879s by dhcpcd.service!

So, most of the time is spent waiting for the network!

2. Why?

My first question was: “Why is the network in the critical path of the boot process?”

And then: “If I cannot change that, can it be made faster?”

3. Solutions

First, we can disable network-online.target from the critical path of the boot:

systemd.services.NetworkManager-wait-online.enable = false;

However, the system will quickly boot up to the tty, and then the GUI will appear 1 or 2 seconds later, so it’s not that great a fix IMO.

Let’s assume we keep it then.

In this case, I know dhcpcd is waiting for any interface to have an IP assigned. This can be changed thanks to dhcpcd.wait:

networking = {
  # ...
  # no need to wait interfaces to have an IP to continue booting
  dhcpcd.wait = "background";
};

Changing this leads to a 50% improvement (from +10.879s to +5.216s):

graphical.target @7.299s
└─multi-user.target @7.298s
 └─network-online.target @7.298s
  └─NetworkManager-wait-online.service @2.081s +5.216s
   └─NetworkManager.service @2.015s +61ms
    └─network-pre.target @2.012s
     └─resolvconf.service @1.979s +31ms
      └─basic.target @1.961s
       └─sockets.target @1.959s
        └─pcscd.socket @1.958s
         └─sysinit.target @1.946s
          └─systemd-timesyncd.service @1.917s +27ms
           └─systemd-tmpfiles-setup.service @1.907s +7ms
            └─local-fs.target @1.905s
             └─run-user-132.mount @2.332s
              └─local-fs-pre.target @215ms
               └─systemd-remount-fs.service @184ms +28ms
                └─systemd-journald.socket @161ms
                 └─-.mount @151ms
                  └─-.slice @151ms

Next, it’s waiting for the network to see if the IP it has got is not already owned by another host. On my local network, this should never be an issue, so let’s deactivate That:

networking = {
  # ...
  # avoid checking if IP is already taken to boot a few seconds faster
  dhcpcd.extraConfig = "noarp";
};

Rebooting, shows another 50% improvement (from +5.216s to +2.573s):

graphical.target @4.681s
└─multi-user.target @4.680s
 └─network-online.target @4.680s
  └─NetworkManager-wait-online.service @2.106s +2.573s
   └─NetworkManager.service @2.037s +64ms
    └─network-pre.target @2.034s
     └─resolvconf.service @2.001s +30ms
      └─basic.target @1.982s
       └─sockets.target @1.981s
        └─pcscd.socket @1.979s
         └─sysinit.target @1.968s
          └─systemd-timesyncd.service @1.937s +28ms
           └─systemd-tmpfiles-setup.service @1.927s +7ms
            └─local-fs.target @1.925s
             └─run-user-132.mount @2.349s
              └─local-fs-pre.target @212ms
               └─systemd-remount-fs.service @181ms +29ms
                └─systemd-journald.socket @159ms
                 └─system.slice @148ms
                  └─-.slice @148ms

That’s good enough for the effort, so I’ll leave it at that.