1.) Make sure you are using `npm ci` instead of `npm install`. The `ci` install command is often much faster and skips a lot of stuff that `install` does. It's also better at sticking purely to your package-lock.json versions. I suggested on UserVoice at one point that Azure Pipelines should switch to `npm ci` as the npm task default. `npm ci` is a relatively recent npm command and I don't everyone has caught on to it yet.
2.) You can use Azure Artifacts for NPM packages. It acts as an alternate NPM feed and caches packages to your Azure DevOps account. Artifacts only supports NPM and NuGet, so it isn't a general caching solution, but in this specific case it helps. I've noticed the builds I have using Artifacts are a bit faster than going straight to NPM. (I started using Artifacts for private NPM packages, so I'm not using it in every build yet.) The current biggest caveat to using Artifacts as your main NPM feed for a project is that it doesn't support NPM audits currently. (I also made sure to report that on UserVoice.)