Knockout.js Modules and Lazy Loading — Structuring Large Applications
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
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.
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.
Hard-coding template paths - Template paths should be relative to the base URL configured in RequireJS, not absolute. Use
text!plugin for HTML templates.Not handling module load errors - RequireJS has error handling. Register a callback for failed module loads to show a friendly error message.
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
- What is the advantage of AMD modules over a single-file Knockout application?
- How does RequireJS handle lazy loading of ViewModel modules?
- What is the purpose of the
text!plugin in RequireJS? - How do you organize a large Knockout application by feature?
- 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
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