Skip to content

Knockout.js Modules and Lazy Loading — Structuring Large Applications

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Knockout.js Modules and Lazy Loading. We cover key concepts, practical examples, and best practices to help you master this topic.

Knockout.js module patterns use AMD (RequireJS) or ES modules to split applications into separate files, enabling lazy loading, dependency management, and cleaner code organization for large-scale apps.

What You'll Learn

  • Structuring Knockout apps with AMD modules
  • Lazy loading ViewModels and components
  • Using RequireJS for dependency management
  • Organizing code into models, ViewModels, services
  • Building with modern ES modules and bundlers

Why It Matters

Single-file Knockout applications become unmanageable beyond a few hundred lines. Modules let you split code by feature, load only what is needed, and manage dependencies explicitly. This scales to applications with hundreds of ViewModels.

Real-World Use

A project management app with 50+ screens organized into modules: Dashboard (dash-viewmodel.js), Projects (project-list.js, project-detail.js), Tasks (task-board.js, task-form.js), and Admin (admin-users.js). Each module loads only when the user navigates to that section.

Module Architecture

flowchart TD
    A[main.js] --> B[App Shell]
    B --> C[Module: Dashboard]
    B --> D[Module: Projects]
    B --> E[Module: Tasks]
    B --> F[Module: Admin]
    D --> G[project-list.js]
    D --> H[project-detail.js]
    E --> I[task-board.js]
    E --> J[task-form.js]
    F --> K[admin-users.js]
    style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

AMD with RequireJS Setup

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
  <script>
    require.config({
      paths: {
        'knockout': 'https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout',
        'jquery': 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min'
      }
    });
  </script>
  <script src="app/main.js"></script>
</head>
<body>
  <div id="app">
    <div data-bind="template: { name: currentView, data: currentData }"></div>
  </div>
</body>
</html>

Main Entry Point

// app/main.js
define(['knockout', 'app/shell'], function(ko, Shell) {
  var shell = new Shell();
  ko.applyBindings(shell, document.getElementById('app'));
});

Shell ViewModel (App Router)

// app/shell.js
define(['knockout', 'app/router'], function(ko, Router) {
  return function Shell() {
    var self = this;
    self.currentView = ko.observable('welcome-template');
    self.currentData = ko.observable(null);
    self.router = new Router(self);

    self.navigateTo = function(route) {
      self.router.go(route);
    };
  };
});

Feature Module: Project List

// app/projects/project-list.js
define(['knockout', 'app/services/api'], function(ko, api) {
  return function ProjectListViewModel() {
    var self = this;
    self.projects = ko.observableArray([]);
    self.loading = ko.observable(true);
    self.searchQuery = ko.observable('');

    self.filteredProjects = ko.pureComputed(function() {
      var query = self.searchQuery().toLowerCase();
      if (!query) return self.projects();
      return self.projects().filter(function(p) {
        return p.name().toLowerCase().indexOf(query) !== -1;
      });
    });

    self.init = function() {
      self.loading(true);
      api.getProjects().then(function(data) {
        self.projects(data.map(function(p) {
          return { id: p.id, name: ko.observable(p.name), status: ko.observable(p.status) };
        }));
        self.loading(false);
      });
    };

    self.init();
  };
});

Router for Module Loading

// app/router.js
define(['knockout'], function(ko) {
  return function Router(shell) {
    var self = this;
    self.routes = {
      'dashboard': { template: 'dashboard-template', module: null },
      'projects': { template: 'projects-template', module: 'app/projects/project-list' },
      'tasks': { template: 'tasks-template', module: 'app/tasks/task-board' }
    };

    self.go = function(routeName) {
      var route = self.routes[routeName];
      if (!route) return;

      shell.currentView(route.template);

      if (route.module) {
        require([route.module], function(Module) {
          var vm = new Module();
          shell.currentData(vm);
        });
      } else {
        shell.currentData(null);
      }
    };
  };
});

ES Module Pattern (Modern Bundlers)

For projects using Webpack, Rollup, or Vite, use ES modules:

// src/viewmodels/UserList.js
import ko from 'knockout';
import { userService } from '../services/userService';

export class UserList {
  constructor() {
    this.users = ko.observableArray([]);
    this.loading = ko.observable(true);
  }

  async load() {
    this.loading(true);
    const data = await userService.getAll();
    this.users(data.map(u => ({
      id: u.id,
      name: ko.observable(u.name),
      email: ko.observable(u.email)
    })));
    this.loading(false);
  }
}

Lazy Loading Components with AMD

// app/components.js
define(['knockout'], function(ko) {
  // Register component with lazy-loaded ViewModel
  ko.components.register('user-profile', {
    viewModel: { require: 'app/users/user-profile-vm' },
    template: { require: 'text!app/users/user-profile.html' }
  });
});

Service Modules

// app/services/api.js
define(['jquery'], function($) {
  var baseUrl = '/api/';

  return {
    getProjects: function() {
      return $.get(baseUrl + 'projects');
    },
    getTasks: function(projectId) {
      return $.get(baseUrl + 'projects/' + projectId + '/tasks');
    },
    saveUser: function(userData) {
      return $.ajax({ url: baseUrl + 'users', method: 'POST', data: userData });
    }
  };
});

Module Organization Best Practices

app/
  main.js                    # Entry point
  shell.js                   # App shell ViewModel
  router.js                  # Client-side router
  models/
    project.js               # Project data model
    task.js                  # Task data model
    user.js                  # User data model
  viewmodels/
    dashboard.js             # Dashboard ViewModel
    project-list.js          # Project list ViewModel
    project-detail.js        # Project detail ViewModel
    task-board.js            # Task board ViewModel
  services/
    api.js                   # API service
    auth.js                  # Authentication service
    storage.js               # Local storage service
  bindings/
    tooltip.js               # Custom tooltip binding
    chart.js                 # Custom chart binding
    sortable.js              # Drag-and-drop binding
  templates/
    dashboard.html           # Dashboard template
    project-list.html        # Project list template
    project-detail.html      # Project detail template

Common Mistakes

  1. Circular dependencies - Module A requires B, and B requires A. Restructure to extract shared code into a third module that both A and B depend on.

  2. Loading all modules at startup - The purpose of AMD is lazy loading. Only load modules when they are needed, not in the main entry point.

  3. Hard-coding template paths - Template paths should be relative to the base URL configured in RequireJS, not absolute. Use text! plugin for HTML templates.

  4. Not handling module load errors - RequireJS has error handling. Register a callback for failed module loads to show a friendly error message.

  5. Global state in modules - Each module should be self-contained. Avoid sharing mutable state through global variables; use services or the DI pattern.

Practice Questions

  1. What is the advantage of AMD modules over a single-file Knockout application?
  2. How does RequireJS handle lazy loading of ViewModel modules?
  3. What is the purpose of the text! plugin in RequireJS?
  4. How do you organize a large Knockout application by feature?
  5. What Dependency Injection patterns work well with AMD modules?

Challenge: Convert a single-file Knockout application with 5 ViewModels into an AMD module structure. Each ViewModel should be in its own file with proper dependency declarations. Implement lazy loading so that each module loads only when the user navigates to that section.

FAQ

Can I use Knockout with modern ES module bundlers like Webpack?

Yes. Knockout works with any module bundler. Import knockout as an ES module: import ko from 'knockout'. Configure the bundler to handle text! template requires or use inline templates.

Does Knockout have a built-in module system?

No. Knockout is agnostic about module loading. Use RequireJS for AMD, Webpack/Rollup for ES modules, or a simple script tag approach for small projects.

How do I share state between modules?

Use a shared service module that multiple ViewModels import. For cross-module events, use ko.subscribable as a pub/sub event bus.

Should I use RequireJS or a bundler for new projects?

For new projects, prefer a bundler like Webpack or Vite. They provide ES module syntax, tree-shaking, and better developer experience. Use RequireJS for legacy or no-build-step projects.

How do I handle circular dependencies in RequireJS?

Export an interface or class from a common module that both depend on, or use RequireJS's exports dependency type to break cycles.

Mini Project

Build a modular CRM application with three feature modules: Contacts, Deals, and Tasks. Each module should be lazily loaded via AMD when the user clicks its tab. Include a shared service module for API calls and a shared binding module for custom date formatting.

What's Next

Put everything together in a complete Knockout.js project that demonstrates all patterns: observables, computeds, components, custom bindings, validation, testing, and modular architecture.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro