remix: Vite with Large Apps: Very slow initial request and slow subsequent requests
What version of Remix are you using?
2.2.0
Are all your remix dependencies & dev-dependencies using the same version?
- Yes
Steps to Reproduce
Adding per a Discord discussion with @pcattori. Given a large app with many routes and components, the first page request after starting vite dev can result in hundreds of network requests. In my case most pages make around 250-300 requests, but another person in that same discussion had close to 600. In addition, every following request to any route (via navigation or actions/loaders) has a significant delay (2+ seconds per page for me). Including both of these issues together since they seem closely related in the way the plugin works.
Slow Initial Request
In my case, the first page request stalls for around 12 seconds while vite does its thing. After this, the app appears to load, but is not responsive for another 12 seconds while it runs 250 network requests for individual components and dependencies. This seems to be a common issue with Vite in dev mode due to the lack of bundling. But there may be some optimization opportunities on the remix plugin side.
I first ran vite dev and loaded a page, which let vite do it’s initial new dependencies optimized... thing. I then restarted vite and ran vite dev --profile, made a browser request, then loaded the profile result into speedscope.app. Looking at it in Left Heavy mode, the time spent in resolveConfig and then flatRoutes is close to 9 seconds total. This is because resolveConfig is being called every time the plugin is loaded, which happens for each individual request. So even though it runs relatively quickly (around 30ms in my case), it’s running hundreds of times and returning the same result every time. My initial thought was to cache the results of resolveConfig, but adding/renaming routes will break.
The next largest bottleneck in the remix plugin is at the transform function for remix-remix-react-proxy. For my initial page load, it’s spending around 3-4 seconds here, most likely due to parsing the code of every single file in my project to see if it has “@remix-run/react” in it.
Slow Subsequent Requests
The transform bottleneck above is also causing subsequent requests to have a delay of 2+ seconds for me, which includes any navigation or calling any loaders/actions. This also slows down HMR – even though vite almost instantly sends the updated component to the browser, rendering is paused while HDR re-runs the loader.
The plugin is set to invalidate the server build on every request, which causes all of the routes to be re-transformed. Since it’s only re-transforming the routes and not every component, it is only taking 2 seconds instead of 3-4 like the initial page load, but it’s enough to make the app feel very sluggish in development.
I tried commenting out the code that invalidates the server modules which makes the app very responsive after the initial load. But it has the same downside as caching resolveConfig: I now have to restart vite dev any time I add or rename routes. @pcattori mentioned figuring out a way to only invalidate when the changed files include routes or the config.
Expected Behavior
Reasonably quick development mode. Definitely not expecting the performance of a small 3-page app, but right now it’s a significant slowdown from the old remix dev server (which took around 10 seconds for the initial build/page load).
Actual Behavior
25 second initial loads and sluggish navigation on following loads.
About this issue
- Original URL
- State: closed
- Created 8 months ago
- Reactions: 9
- Comments: 19 (13 by maintainers)
@dmarkow I was running at ~5m 45s. Now 22s!
An added bonus is that fixing these things drastically improves builds. Before any patches,
vite build && vite build --ssrwas taking my app 129 seconds. After patches (including a fix for the RefreshRuntime thing above), it’s down to 24 seconds.I think this can be closed now that both the LiveReload fix and my caching PR are merged.
@ZipBrandon Here is a patch file against 2.2.0 that includes the LiveReload fix. I made a PR yesterday for my other changes, so if they get accepted before 2.2.1 you won’t need this patch anymore.
@dmarkow Thanks for picking that up! I’ve just opened a PR addressing this: https://github.com/remix-run/remix/pull/7883
@pcattori Is there a reason the RefreshRuntime is being injected into every file that imports from @remix-run/react? Shouldn’t it just be the root file? Making it just inject into the root route will mostly eliminate the Babel slowdown. I’m wondering if this should actually be:
I think I figured out my pain points above. I ended up adding two caches. The first one is for the middleware, which checks and refreshes the cache on each request, and only invalidates the virtual modules if the config changed. This makes site navigation a ton more responsive in dev, and speeds up HDR quite a bit. The other cache is initially set in the plugin config and used anywhere there are excessive calls to
resolvePluginCache(). This cache gets refreshed inremix-hmr-updates, which should be the first part of the plugin to run anytime there is a file change as far as I can tell.So far, this caching has drastically reduced that 8.5 seconds spent in
resolveConfigdown to 146ms. My next bottleneck on the profile graph isreplaceImportSpecifierbeing called for a bunch of files inremix-remix-react-proxy(3+ seconds). The slowness here is Babel’sparse/traverse/generatewhich I’m not sure how we speed up without moving away from Babel completely. Maybe switching to something faster like SWC?I have a patch with the caching, I’d like to use it locally for a little bit before I create a PR around this. All Vite integration tests are still passing. The only jankiness I’m seeing is that adding a new route while the server is running and then trying to navigate to it sometimes causes an error, but if I hard-reload the browser it comes up fine without having to restart the dev server. And I’m getting the same issue without my patch which may warrant a separate issue.
Here’s the patch if anyone wants to test – you can use patch-package and drop it in your project’s
patchesfolder. @ZipBrandon I’d be curious to see what kind of speed increases you see on initial build.@mikekidder Vite itself may not, but Remix does use a babel transform to 1) inject react-refresh code and 2) remove server exports (loader/action/headers) from route files.