Skip to content

♻️ Refactor route preload and component loading with core packs#2734

Open
b-l-i-n-d wants to merge 16 commits into
4.0.0-devfrom
v4-performence
Open

♻️ Refactor route preload and component loading with core packs#2734
b-l-i-n-d wants to merge 16 commits into
4.0.0-devfrom
v4-performence

Conversation

@b-l-i-n-d
Copy link
Copy Markdown
Collaborator

@b-l-i-n-d b-l-i-n-d commented May 19, 2026

Code Splitting Architecture in Tutor LMS

This walkthrough explains how code-splitting is implemented in the Tutor LMS frontend using Rspack, TypeScript, and dynamic imports. Code splitting significantly improves website performance by breaking down large JavaScript bundles into smaller chunks, loading only the code necessary for the user's current view or interaction.

Overview

The code splitting architecture in this repository operates on two primary levels:

  1. Route-Based Splitting: Pages like the Dashboard and Learning Area are split into individual chunks based on their subpages (e.g., my-courses, quiz-attempts).
  2. Core Pack Splitting: Heavy shared functionalities (e.g., media editors, form controls) are abstracted into "Core Packs" and loaded only when a specific route explicitly requests them.

High-Level Architecture Diagram

graph TD
    A["Browser Loads Entry Point<br/>(e.g., tutor-dashboard.js)"] --> B["getCurrentPage()<br/>Determine active subpage"]
    B --> C["getDashboardRouteConfig()<br/>Lookup route configuration"]
    C --> D["registerRoutePreload()<br/>Trigger parallel preloading"]

    subgraph Parallel Network Requests
        D --> E["Extract requested Core Packs<br/>(e.g., ['core-form-controls'])"]
        D --> F["Extract Route Loader<br/>(e.g., import('./pages/settings'))"]
        
        E --> G["requestCorePacks()<br/>Updates window.TutorRequestedCorePacks"]
        G --> H["window.TutorPreloadCorePacks()"]
        
        H -.-> |"Rspack dynamic import()"| I[("Download core pack chunk<br/>e.g., tutor-core-form-controls.js")]
        F -.-> |"Rspack dynamic import()"| J[("Download route chunk<br/>e.g., tutor-dashboard-settings.js")]
    end

    subgraph Initialization Sequence
        I --> K["Core Packs Promise"]
        J --> L["Route Module Promise"]
        
        K --> M{"chainRoutePreload()<br/>Promise.all([...])"}
        L --> M
        
        M --> N["alpine:init / DOMContentLoaded event"]
        N --> O["initializeDashboard() & initializePlugin()"]
        
        O --> P["Await window.TutorRoutePreload<br/>(Wait for network requests)"]
        P --> Q["registerOptionalCorePacks()<br/>Injects into TutorComponentRegistry"]
        Q --> R["Alpine.start()"]
        R --> S["initializeDashboardRoute()<br/>Executes route-specific logic"]
        S --> T((Application Ready))
    end
Loading

1. Rspack Configuration

Code splitting is fundamentally enabled by the bundler. In rspack.config.mjs, specific optimizations and chunking strategies are defined:

Tip

Dynamic Chunk Naming: Notice the use of chunkFilename: 'js/lazy-chunks/[name].js'. This ensures that all dynamically imported files are separated from the main entry points and placed in a dedicated directory.

  • Split Chunks: The optimization.splitChunks configuration targets async chunks.
  • Shared Async Modules: The tutorAsyncCommon cache group automatically extracts shared dependencies across multiple async chunks into a common file if they are used at least twice. This prevents code duplication across different lazy-loaded pages.

2. Core Pack Code Splitting

Not every page needs a rich text editor or complex form controls. In assets/core/ts/index.ts, core functionalities are mapped to dynamic imports:

const optionalCorePackLoaders: Record<OptionalTutorCorePackName, () => Promise<CorePackModule>> = {
  'core-form-controls': async () => {
    const module = await import(/* webpackChunkName: "tutor-core-form-controls" */ '@Core/ts/packs/form-controls');
    return { register: module.registerCoreFormControlsPack };
  },
  // ...
};

Using the /* webpackChunkName: "..." */ magic comment tells Rspack exactly what to name the generated chunk file. When preloadOptionalCorePacks is called, it triggers these promises, downloading the network payload only when necessary.

3. Route-Based Code Splitting

The entry points for major SPA-like areas (like the Instructor Dashboard and Learning Area) do not include the code for all their tabs.

In assets/src/js/frontend/dashboard/index.ts and assets/src/js/frontend/learning-area/index.ts:

const dashboardRoutes: Record<string, TutorRouteConfig<DashboardRouteModule>> = {
  'my-courses': createRouteConfig(withBasePack(), async () => {
    const { initializeMyCourses } = await import(
      /* webpackChunkName: "tutor-dashboard-my-courses" */ './pages/my-courses'
    );
    return { initializeDashboardRoute: initializeMyCourses };
  }),
  settings: createRouteConfig(withBasePack('core-form-controls', 'core-media-editor'), async () => {
    const { initializeSettings } = await import(/* webpackChunkName: "tutor-dashboard-settings" */ './pages/settings');
    return { initializeDashboardRoute: initializeSettings };
  }),
};

Note

The settings route explicitly requires core-form-controls and core-media-editor. If the user navigates to the settings page, the route config guarantees that both the settings chunk and the required core packs are fetched.

4. Preloading Infrastructure

To prevent a waterfall of network requests (e.g., waiting for a route to load before realizing it needs a core pack), the application coordinates preloading using assets/src/js/frontend/route-preload.ts.

The registerRoutePreload function is invoked immediately upon script execution. It uses Promise.all to fetch both the route module chunk and the requested core pack chunks in parallel:

export const registerRoutePreload = <TModule>(...) => {
  const preloadedRouteModule = routeConfig ? routeConfig.load() : null;
  const corePackPreload = requestCorePacks(routeConfig?.packs || defaultPacks);

  // Chains both promises so the application only initializes when everything is ready
  return chainRoutePreload(corePackPreload, preloadRoute());
};

Performance Benefits

  1. Reduced Initial TTI (Time to Interactive): The tutor-dashboard.js and tutor-learning-area.js entry points remain lightweight because they only contain the routing logic and common infrastructure.
  2. Efficient Caching: Because chunks are split by feature, an update to the "quiz" code only changes the tutor-learning-quiz.js chunk. The browser can continue to use the cached versions of other pages.
  3. Bandwidth Savings: Students only download the code for the specific view they are interacting with.

@b-l-i-n-d b-l-i-n-d marked this pull request as ready for review May 20, 2026 10:41
@b-l-i-n-d b-l-i-n-d added enhancement New feature or request QoL Quality of Life 4.0.0 Tutor v4.w0w labels May 20, 2026
Copy link
Copy Markdown
Collaborator

@sazedul-haque sazedul-haque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make a separate PR for comment updates @b-l-i-n-d

return params.get('subpage');
};

const preloadedLearningAreaPage = getCurrentLearningAreaPage();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name should be currentPage or currentLearningAreaPage

return dashboardRoutes[route];
};

const preloadedDashboardRoute = getCurrentPage();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

variable name should be currentPage.

@@ -0,0 +1,8 @@
import { type TutorComponentRegistry } from '@Core/ts/ComponentRegistry';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need the player in course details page in future.

selectMeta,
selectDropdownMeta,
statusSelectMeta,
stepperDropdownMeta,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no use of stepperDropdownMeta. Let's skip this.

initializeDashboardRoute: initializeHome,
};
}),
dashboard: createRouteConfig(withBasePack('core-form-controls'), async () => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks duplicate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.0.0 Tutor v4.w0w enhancement New feature or request QoL Quality of Life

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants