Mostly it's Docker-specific, but all of the things causing this latency tend to be what makes Docker useful in the first place. In particular, networking and mounts.
For starters, if you're just running the typical Docker set up to talk back and forth using a UNIX or TCP socket, all of _that_ networking and HTTP/JSON encoding+decoding will add some overhead. This is even worse if you're going through a proxy layer like the socket which Docker for Mac creates and forwards to the Linux VM. OK, so there's at least a few milliseconds of latency built in for this, the socket forwarding hop on my Docker for Mac seems to add ~5-15ms for instance.
Once the daemon receives the request, runc, the command that actually does the work to create a container without all that hoopla, is invoked.
Then, to actually start up a container quite a few mounts get set up either for devices (potentially including tty in the case of `run -t`) or for the union filesystem. Take a look at the output of `mount` in a container sometime, around 30-40 mounts (and all those overlay layers...) and a lot of that gets set up on the fly because each container has its own unique mount namespace and view of the world.
Then after all of that, you need to set up networking too (`docker run --net none` will skip this), otherwise containers can't talk to the Internet, to each other, or listen on ports of the host. Remember, each has its own network namespace, created from scratch. So, Docker's doing all manner of adding port proxies, modifying iptables, attaching containers to interfaces, and so on. Otherwise, `docker run alpine wget -qO- https://google.com` or `docker run --net=internalnet` wouldn't work out of the box.
The work isn't easily done concurrently since there are so many dependencies - e.g., you need to have a process in order to set a network namespace, in order to run that process, you need to have a root FS prepared to pivot root into, and so on.
Meanwhile to all of this happening the Docker daemon is doing god knows what, it could be pulling images, or running other containers already, supervising processes, etc. While many of those things are concurrent due to use of goroutines, they do eat up resources and generally too much work saturates and slows down the daemon. That's not really unique to Docker though, any program doing all this stuff at once would have that issue.
Anyway, that's why Docker takes way longer to start a process than good old execve() in your shell.