remix icon indicating copy to clipboard operation
remix copied to clipboard

Purge cache on file changes on dev servers

Open na2hiro opened this issue 3 years ago • 4 comments

Closes: -

  • [ ] Docs
  • [x] Tests

Testing Strategy:

  • remix-dev (remix template): Added dev-server-test.ts integration test file to verify live reload works
  • express template: Tested in my project which is based on remix express template

Background

For the smooth live reload on dev server, express template and remix dev commands both has a mechanism to purge cache of server-side bundle built by Remix compiler, named purgeRequireCache(). This is currently called for every HTTP request.

function purgeRequireCache() {
  // purge require cache on requests for "server side HMR" this won't let
  // you have in-memory objects between requests in development,
  // alternatively you can set up nodemon/pm2-dev to restart the server on
  // file changes, but then you'll have to reconnect to databases/etc on each
  // change. We prefer the DX of this, so we've included it for you by default
  for (const key in require.cache) {
    if (key.startsWith(BUILD_DIR)) {
      delete require.cache[key];
    }
  }
}
app.all(
  "*",
  process.env.NODE_ENV === "development"
    ? (req, res, next) => {
        purgeRequireCache();

        return createRequestHandler({
          build: require(BUILD_DIR),
          mode: process.env.NODE_ENV,
        })(req, res, next);
      }
    : createRequestHandler({
        build: require(BUILD_DIR),
        mode: process.env.NODE_ENV,
      })
);

(From templates/express/server.js)

Problem

However, this solution is not scalable in following ways and it's easy to get really slow compared to production builds:

  • As the app grows, size of build/index.js grows and it takes longer and longer to parse and execute. We don't want to repeat unnecessary re-computation.
    • In my project, app directory is 600KB (9.5K lines), and the resulted bundle has 243KB (5K lines).
  • As the nested routes get deeper, the purge happens multiple times per one navigation, because one navigation can make multiple requests to loaders for the ancestor routes
  • If we have resource routes which are loaded by a page, then it can create huge amount of HTTP requests, each of which causes the purge as well.

In my project, each HTTP request takes like 400ms, which can easily lead to 1s for page navigation depending on routes.

Solution

Move purge call from HTTP request handler to...

  • remix-dev: after build is done, right before sending reload event to browser
  • express template: after build is done, watched by chokidar
    • Browser can be notified to reload and hit server before chokidar notifies to purge cache. 100ms delay is added before reload.

I didn't find integration tests for dev server, so I added one to test live reload happens after file changes.

References

na2hiro avatar Oct 30 '22 10:10 na2hiro

🦋 Changeset detected

Latest commit: d23af703dd32aeb38faab8bda95cd231ade19343

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@remix-run/dev Patch
create-remix Patch
remix Patch
@remix-run/architect Patch
@remix-run/cloudflare Patch
@remix-run/cloudflare-pages Patch
@remix-run/cloudflare-workers Patch
@remix-run/deno Patch
@remix-run/eslint-config Patch
@remix-run/express Patch
@remix-run/netlify Patch
@remix-run/node Patch
@remix-run/react Patch
@remix-run/serve Patch
@remix-run/server-runtime Patch
@remix-run/vercel Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

changeset-bot[bot] avatar Oct 30 '22 10:10 changeset-bot[bot]

I think this will also help for support with in-memory caches / databases / stores / etc... on the server in dev. Currently, any in-memory data gets cleared out when a page transition occurs. Same thing with connections to databases, where we used to rely on global vars. Going to verify this tomorrow.

pcattori avatar Oct 30 '22 20:10 pcattori

Any objects that need to survive the purge (like dB connection or cache) should be stored on the global object in development.

I have a similar solution for purges, but I created a special endpoint /build/__purge__ that is called by my custom <LiveReload>

fetch('/build/__purge__')
 .finally(() => location.reload())

kiliman avatar Oct 30 '22 21:10 kiliman

I created a related PR, https://github.com/remix-run/remix/pull/4307, meant to provide a stopgap against this same pain point, i.e. doing some work after builds complete.

In this reference PR, I also briefly touch on the idea that it would be great to have an API within the remix.config.js that allows users to hand a function to the Remix compiler that is run as part of the esbuild lifecycle callbacks.

andrewbrey avatar Oct 31 '22 16:10 andrewbrey

I've added a proposal for a Node API for the compiler and dev server that I think will address the fundamental problem.

pcattori avatar Nov 16 '22 21:11 pcattori

The proposal looks good. Looking forward to it being merged. Thanks a lot for digging into this!

na2hiro avatar Nov 21 '22 00:11 na2hiro

Superceded by #5133

pcattori avatar Jan 21 '23 06:01 pcattori