T
TanStack2mo ago
extended-salmon

(Lazy) load routes from other chunks at runtime

Heya, I have an application that I've split into several layers (depending on the features available to the user) which I've told Vite to bundle into chunks and then lazy-load them when required (lazy(() => import('./layer-1'))). Because of regulations, I need to keep those chunks separate, what goes on in layer-1 must only be inside layer-1 and not leak out. This includes route data, too. I could lazy load the components but the information about the route/the loaders it uses would still be bundled in the initial bundle if I got it right, which I'm not allowed to do. I've looked into code splitting but it seems like it's more about distributing the route data and less about putting it in different independent bundles. Is it possible to lazy load routes at runtime (e.g. signal based)? It doesn't matter if the routes can't be removed later on, the requirement is just to suppress the initial load. What I would like to do is something like
createEffect(() => {
if (hasLayerAccess('layer1'))
router.addRoutes(lazy(() => import('./layer-1/routes')));
});
createEffect(() => {
if (hasLayerAccess('layer1'))
router.addRoutes(lazy(() => import('./layer-1/routes')));
});
It would be nice if static typing works, but I could also live without, because the links between layers are very sparse and could just be a typeless hack if needs be. Currently with solid-router apparently this kind of works with something like <Show when={hasAccess()}>{lazy(() => import...)}</Show>, but I kinda need to migrate away from solid-router.
8 Replies
typical-coral
typical-coral2mo ago
Automatic Code Splitting | TanStack Router React Docs
The automatic code splitting feature in TanStack Router allows you to optimize your application's bundle size by lazily loading route components and their associated data. This is particularly useful...
extended-salmon
extended-salmonOP2mo ago
It might, I haven't used file-based routing so far but rather went with the coded config approach instead (because my current setup wasn't made with that in mind and so I've organized the files in a more logical rather than route based way). I've ruled it out so far, but if it solves my chunking issues I'll give it a go. I'm going to run some experiments and report back. Thank you so far! I've tried it but I don't think it's working. import { routeTree } from './routeTree.gen'; will include all route data, including components, because the .gen file imports all the routes (which in turn contain al the components). This means that my entrypoint bundle already contains all the other bundles, and nothing is really lazy loaded at all - it's all pulled in and loaded in one go:
// app-XXX.js
import './modulepreload-polyfill.js-C5t25fxl.js';
import { a as createRouter, r as render, b as createComponent, R as RouterProvider } from './lib/generic-DunOjjjC.js';
import { R as Route, a as Route$1, b as Route$2 } from './routes-BaLi0GLd.js';
import { R as Route$3, a as Route$4, b as Route$5 } from './acp-BVJTGDg8.js';
import './preload-helper.js-CWZBUsdZ.js';

const AboutRoute = Route.update({
id: "/about",
path: "/about",
getParentRoute: () => Route$1
});
const AcpIndexRoute = Route$3.update({
id: "/acp/",
path: "/acp/",
getParentRoute: () => Route$1
});
// app-XXX.js
import './modulepreload-polyfill.js-C5t25fxl.js';
import { a as createRouter, r as render, b as createComponent, R as RouterProvider } from './lib/generic-DunOjjjC.js';
import { R as Route, a as Route$1, b as Route$2 } from './routes-BaLi0GLd.js';
import { R as Route$3, a as Route$4, b as Route$5 } from './acp-BVJTGDg8.js';
import './preload-helper.js-CWZBUsdZ.js';

const AboutRoute = Route.update({
id: "/about",
path: "/about",
getParentRoute: () => Route$1
});
const AcpIndexRoute = Route$3.update({
id: "/acp/",
path: "/acp/",
getParentRoute: () => Route$1
});
at the very least, the route information becomes available this way, which is what I want to prevent, but seeing as it's importing the acp js too, it means it'll load that immediately on app start as well - so it's in separate files, but still all loaded at the same time, which makes it kinda pointless for me
typical-coral
typical-coral2mo ago
did you enable autoCodeSplitting ?
extended-salmon
extended-salmonOP2mo ago
export default defineConfig({
plugins: [
tanstackRouter({
target: 'solid',
autoCodeSplitting: true
}),
export default defineConfig({
plugins: [
tanstackRouter({
target: 'solid',
autoCodeSplitting: true
}),
typical-coral
typical-coral2mo ago
this will still result in all routes being "available" however the components are split off check the actual chunks
extended-salmon
extended-salmonOP2mo ago
the acp.js which is included contains the component code hm, maybe I need to split this up let me try moving the components somewhere else actually I can't do that because it's using just the RouteComponent function, is it? what I mean is: I do the chunking by telling rollup to create manual chunks based on filenames - i.e. everything inside /acp/ gets thrown into the acp-X.js since that contains the route information and the component, it means that both are bundled in the same file, so even if tanstack just loads the route information, the component gets inevitably pulled in, too let me see if I can defuse this by checking for ?tsr-split=component okay, yeah, that worked - had to tell rollup to treat those queries differently not super happy that the routes are still available on the client, but at least the components themselves would be separate now
typical-coral
typical-coral2mo ago
isnt hiding the routes on the client rather "security through obscurity"? why do you need manual chunking?
extended-salmon
extended-salmonOP2mo ago
it kind of is, but security through obscurity isn't bad if it's an additional, and not the only, layer. if you don't know how an application works, and what it can do, it becomes harder to exploit/copy it. at the end of the day, most of the stuff we do in that regard is always just raising the bar high enough so your average adversary gets fed up/runs out of time and goes somewhere else. in my case, if the logic or functionality of the app were easily known by just going to the url and looking at the JS, it could be rather bad. there are actors that would love to know how e.g. the backend is configured, because this could help them game the system that I'm making the backend filters access to chunks. if I use automatic chunks, vite throws them into random-ish named chunks, usually by the file name, which means I lose all information about where the chunk originally was. by manually chunking it, I can say that all stuff from inside /acp/ goes into acp.js, and can then tell the backend accordingly that acp.js is to be served only to members in the admin group in a way, it's about limiting the potential attack & information surface the ideal way to deal with this problem, of course, would be to just have a separate app for e.g. the acp and whatever else, but then you have to split up the application, and then there's duplicate parts that need to be shared, and it becomes rather complicated in my opinion, especially if the functionality of the normal app is usually also needed to do the acp stuff

Did you find this page helpful?