wmr icon indicating copy to clipboard operation
wmr copied to clipboard

Spread Terser+Brotli npm upgrade work out over time

Open developit opened this issue 5 years ago • 0 comments

The development middleware handles /@npm/FOO dependency requests as fast as possible by returning unminified responses the first time a dependency is requested, and applying only basic gzip compression to responses above a size threshold. As an optimization, all individual dependencies requested are placed into a queue that slowly upgrades their cached code (on disk and in-memory) by minifying it and applying Brotli compression (node's native implementation with q11 setting).

This operation ends up being quite expensive, and for projects with a lot of dependencies it tends to spike CPU usage for a little while after the first time you load the application in a browser (not after starting wmr in dev mode). This happens because the queue is rudimentary, simply processing each dependency in a loop with a 1-second delay between entries:

https://github.com/developit/wmr/blob/cb10267228ea8f02119c0465b7174e0752416828/src/lib/npm-middleware-cache.js#L72-L89

This actually ends up bogging Glitch down, since the 1-second delay is short enough that synchronous Terser passes on deps block the event loop when there are still more dependency requests to be handled by WMR.

Ideally, we can solve this by doing three things:

    • reduce the Brotli quality a bit.

    This could be a user setting, or it could use simple detection of low compression rate (bytes/ms) to automatically reduce quality to prevent high CPU usage.

  1. substantially increase the time required before the optimizer queue processes a dependency.

    Idea: We could create a requestIdleCallback-like function, which lazily calls a callback after a period where no work has been done. To track "work being done", all incoming network requests could increment a centralized pendingWork counter, then decrement it when the request has been handled. Idle time would be the time spent at pendingWork==0.

  2. switch to off-main-thread Terser (~since Terser is synchronous~ its now at least theoretically async)

    Idea: we could create bundle a single terser-worker.js file that includes a copy of Terser and provides a postMessage RPC API that we can then use for all three cases where Terser is currently being used on the main thread: production bundling, our own wmr.cjs bundling, and development npm dep upgrades. It would also move Terser out of the main bundle, saving around 80kb.

It may also be worth considering a flag that turns automatic dependency optimization off: it's a nicety, and for folks on constrained environments like Glitch it may not be worth the overhead.

Checklist

  • [ ] Implement a true idle queue based on observation of request handling
  • [ ] Move Terser into its own bundle and load it in a Worker (worker_threads is Node 10+, which is fine)
  • [ ] Reduce Brotli quality if compression is slow (below a "fast machine" threshold we set)
  • [x] [maybe] Add an --optimize-dependencies flag (update: --compress false now triggers this)

developit avatar Jul 19 '20 20:07 developit