These practices may not be as widespread as I assumed, but this is how I've been doing npm dependencies for the last few years.
Originally we used to simply check in the node_modules folder.
Now I check in the npm-shrinkwrap.json (sanitised via https://www.npmjs.com/package/shonkwrap), and then use a caching proxy between the CI server and the real npm.
There's a bunch of choices available for this proxy, I've used one called nopar, but sinopia is also popular. Both Artifactory and Nexus can also be configured to do this, as well as act as caching proxies for a number of other package systems too.