Bundle 200 AMD modules into 2 HTTP requests — build profiles, layers, closure compiler, CSS optimization, and production-ready deployment
customBase — Strip Unused Dojo CoreinternStrings — Inline TemplatesEach require(["module"]) in an un-built app causes a separate HTTP request. Dojo's AMD loader is lazy — it discovers dependencies at runtime, fetching each one as needed, causing a waterfall of requests.
The build tool concatenates all required modules into "layers" — single JS files that the loader reads from cache instead of fetching individually.
# Prerequisites
# Node.js 12+ (for the closure compiler wrapper)
# Java (for Google Closure Compiler — optional but recommended)
# The full Dojo SDK download (not CDN) — build scripts are in util/
# Directory layout
project/
├── util/ �? Dojo build scripts (from SDK)
│ └── buildscripts/
│ └── build.js
├── dojo/ �? Dojo SDK source
├── dijit/
├── dojox/
├── myapp/ �? Your application
│ ├── main.js
│ └── ...
└── profiles/
└── myapp.profile.js �? YOUR build profile (create this)
# Run the build (from project root)
node util/buildscripts/build.js \
profile=profiles/myapp.profile \
action=release
# Options:
# action=release → full optimized build (use for production)
# action=clean → delete previous build output
# action=help → show all options
# releaseDir=dist → output directory (default: release/)
closure optimizer requires Java. If Java isn't available, use "shrinksafe" — it's JavaScript-based but produces slightly larger output. For most enterprise deployments, Java is available and Closure is preferred.
// profiles/myapp.profile.js — full annotated build profile
var profile = (function() {
return {
// ── Output settings ──────────────────────────────────────────
releaseDir: "dist", // output folder (relative to project root)
action: "release",
// ── JS optimization ──────────────────────────────────────────
// "closure" → Google Closure Compiler (best compression, needs Java)
// "shrinksafe" → Dojo's own minifier (no Java required)
// "comments" → strip comments only (good for debugging builds)
// false → no optimization (development builds)
layerOptimize: "closure",
optimize: "closure", // also optimize individual modules not in layers
// ── CSS optimization ─────────────────────────────────────────
// "comments" → strip comments and inline @import files
// "comments.keepLines" → strip comments, preserve line breaks
// false → no CSS optimization
cssOptimize: "comments",
// ── Locale settings ──────────────────────────────────────────
localeList: "en-us,fr,ja,de", // NLS bundles to include in build
// ── Source map ───────────────────────────────────────────────
// Requires Closure Compiler — maps minified back to original
// useSourceMaps: true,
// ── Packages — must match your dojoConfig ────────────────────
packages: [
{ name: "dojo", location: "dojo" },
{ name: "dijit", location: "dijit" },
{ name: "dojox", location: "dojox" },
{ name: "myapp", location: "myapp" },
{ name: "dgrid", location: "dgrid" },
{ name: "dstore", location: "dstore" },
{ name: "put-selector", location: "put-selector" },
{ name: "xstyle", location: "xstyle" }
],
// ── Layers — the main build artifact ─────────────────────────
// Defined in the next section
layers: {
// ... see section 9.4
},
// ── Per-module overrides ──────────────────────────────────────
// Files to exclude from optimization (e.g., already minified)
files: [
{ name: "myapp/vendor/already-minified.js", noOptimize: true }
]
};
})();
A layer is a single JS file that concatenates many AMD modules. When the browser loads a layer, all those modules are available without individual HTTP requests. Design layers around what loads together.
// profiles/myapp.profile.js — layers section
layers: {
// ── Layer 1: Dojo core ────────────────────────────────────────
// Everything from dojo that the whole app uses
"dojo/dojo": {
include: [
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/_base/array",
"dojo/dom",
"dojo/dom-class",
"dojo/dom-style",
"dojo/dom-construct",
"dojo/dom-attr",
"dojo/on",
"dojo/query",
"dojo/topic",
"dojo/when",
"dojo/Deferred",
"dojo/promise/all",
"dojo/request",
"dojo/store/Memory",
"dojo/store/Observable",
"dojo/Stateful",
"dojo/i18n",
// NLS bundles
"dojo/i18n!myapp/nls/messages"
],
boot: true, // this layer acts as the loader bootstrap
customBase: false // include full dojo/_base (set true to strip unused)
},
// ── Layer 2: Dijit UI components ─────────────────────────────
"myapp/dijit-layer": {
include: [
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin",
"dijit/form/ValidationTextBox",
"dijit/form/Select",
"dijit/form/FilteringSelect",
"dijit/form/DateTextBox",
"dijit/form/Button",
"dijit/form/CheckBox",
"dijit/form/Form",
"dijit/Dialog",
"dijit/layout/BorderContainer",
"dijit/layout/TabContainer",
"dijit/layout/ContentPane",
"dijit/layout/AccordionContainer",
"dijit/Tooltip",
"dijit/Menu",
"dijit/MenuItem"
],
// This layer is separate — loaded after dojo-layer
// Exclusions prevent duplicate code across layers
exclude: ["dojo/dojo"]
},
// ── Layer 3: Application code ─────────────────────────────────
"myapp/main": {
include: [
"myapp/App",
"myapp/views/Dashboard",
"myapp/views/EmployeeList",
"myapp/views/EditForm",
"myapp/widgets/StatusBar",
"myapp/widgets/FilterPanel",
"myapp/stores/EmployeeStore",
"myapp/topics"
// Templates are inlined via internStrings (section 9.8)
],
exclude: [
"dojo/dojo",
"myapp/dijit-layer"
]
}
}
| Pattern | Layers | Best For |
|---|---|---|
| Single bundle | 1 layer with everything | Small apps, intranet, cache-able |
| Core + App | dojo-core layer + app layer | Most enterprise apps — dojo core cached across pages |
| Core + UI + App | 3 layers as above | Large apps with many pages sharing same Dijit set |
| Per-page layers | 1 layer per major page/view | Multi-page apps where views have very different deps |
| shrinksafe | closure (SIMPLE_OPTIMIZATIONS) | closure (ADVANCED_OPTIMIZATIONS) | |
|---|---|---|---|
| Java required | No | Yes | Yes |
| Output size | ~60% reduction | ~70% reduction | ~80% reduction |
| Risk | Very low | Low | High — can rename public APIs |
| Recommended | When Java unavailable | ✅ Standard choice | Only if you fully control all code |
// Profile settings for each optimizer:
// shrinksafe (no Java)
layerOptimize: "shrinksafe",
optimize: "shrinksafe",
// Closure SIMPLE (recommended — safe for Dojo)
layerOptimize: "closure",
optimize: "closure",
// closure.level = "simple" �? default, no need to set
// Closure ADVANCED (dangerous with Dojo — avoid)
layerOptimize: "closure.advanced",
// WARNING: renames object properties — breaks widget APIs
// Only use if you wrap your entire app in Closure annotations
// Profile CSS optimization setting
cssOptimize: "comments", // strips comments + inlines @import
// What it does:
// 1. Strips all CSS comments
// 2. Inlines all @import statements — merges into a single file
// 3. Normalises whitespace
// Does NOT: minify property values or shorten hex colours
// For the Dijit theme CSS, the build inlines all the sub-files:
// claro.css imports: claro/form/*.css, claro/layout/*.css, etc.
// After build: one claro.css with everything inlined
<!-- DEVELOPMENT: load individual theme files -->
<link rel="stylesheet" href="dijit/themes/claro/claro.css">
<link rel="stylesheet" href="myapp/css/main.css">
<!-- PRODUCTION: load built CSS (paths change after build) -->
<link rel="stylesheet" href="dist/dijit/themes/claro/claro.css">
<link rel="stylesheet" href="dist/myapp/css/main.css">
<!-- Or: combine into one CSS layer if you configure a CSS layer in profile:
staticHasFeatures in profile lets you conditionally include CSS based on
has() feature flags -- useful for IE vs modern browser CSS paths -->
customBase — Strip Unused Dojo CoreBy default, dojo/_base (the synchronous compat layer) is included in every build. Setting customBase: true on a layer tells the build to not auto-include dojo/_base — it only includes what your include array explicitly lists.
// Profile layer with customBase
layers: {
"dojo/dojo": {
include: [
"dojo/main", // minimal loader bootstrap
"dojo/_base/lang", // explicitly include what you actually use
"dojo/_base/declare",
"dojo/_base/array",
// Do NOT include dojo/_base/connect, dojo/_base/html, etc.
// if your app doesn't use those legacy APIs
],
boot: true,
customBase: true // �? opt-out of automatic _base inclusion
}
}
// staticHasFeatures tells the build system the answers to dojo/has() checks
// so dead code branches can be eliminated at build time
var profile = {
staticHasFeatures: {
"config-deferredInstrumentation": 0, // remove deferred debug code
"config-tlmSiblingOfDojo": 0,
"dojo-amd-factory-scan": 0,
"dojo-combo-api": 0,
"dojo-config-api": 1, // 1 = keep this feature
"dojo-debug-messages": 0, // 0 = remove debug messages
"dojo-firebug": 0,
"dojo-guarantee-console": 0,
"dojo-has-api": 1,
"dojo-sniff": 0,
"dojo-sync-loader": 0, // remove sync loader (we use async)
"dojo-test-sniff": 0,
"dojo-timeout-api": 0,
"dojo-trace-api": 0,
"dojo-undef-api": 0,
"dojo-v1x-i18n-Api": 0,
"ie": 0, // target modern browsers only
"quirks": 0
}
};
internStrings — Inline TemplatesWithout internStrings, every dojo/text!./templates/MyWidget.html causes a separate HTTP request at runtime (even in a built app). With internStrings: true, the build inlines the template content directly into the JS layer.
// Profile setting
var profile = {
internStrings: true // inline all dojo/text! resources into JS layers
};
// What happens at build time:
// BEFORE (runtime — separate XHR):
// define(["dojo/text!./templates/Card.html"], function(template) { ... })
// → fetches Card.html as a separate HTTP request
// AFTER (build output — template inlined):
// define(["dojo/text!./templates/Card.html"], function(template) {
// // template value is baked into the layer file:
// // "<div class='card'><h3>${name}</h3>...</div>"
// })
// → zero additional HTTP request — template is in the layer JS file
internStrings: true in production builds. In development, templates load via XHR (easier to edit). The same source works for both — the build handles the transition.
| Scenario | Use CDN? | Reason |
|---|---|---|
| Public website, light Dojo usage | Maybe | Browser may have CDN version cached from another site |
| Enterprise intranet app | �?� No | Intranet may not have internet access. Local = reliable. |
| Production app with build layers | �?� No | CDN can't serve your custom layers. Must be local. |
| Quick prototype / demo | ✅ Yes | Zero setup, no download needed |
| App needing offline support | �?� No | CDN unavailable offline |
// DEVELOPMENT: CDN for quick work
var dojoConfig = { async: true, packages: [
{ name: "myapp", location: "/js/myapp" }
]};
// <script src="//ajax.googleapis.com/ajax/libs/dojo/1.17.3/dojo/dojo.js"></script>
// PRODUCTION: local build — all code in your control
var dojoConfig = { async: true, parseOnLoad: false };
// <script src="/static/dist/dojo/dojo.js"></script>
// The built dojo.js contains your app layer as well — no other script needed
// VERSION LOCK — avoid CDN version drift
// If you must use CDN, pin the exact version:
// https://ajax.googleapis.com/ajax/libs/dojo/1.17.3/dojo/dojo.js
// Not: https://ajax.googleapis.com/ajax/libs/dojo/1/dojo/dojo.js �? dangerous
// Development build with no optimization — readable output
// Run this during development to test the build pipeline without minification
node util/buildscripts/build.js \
profile=profiles/myapp.profile \
action=release \
optimize=false \
layerOptimize=false
// Then load the built app in the browser
// Any module errors are easy to trace since code is un-minified
| Problem | Cause | Fix |
|---|---|---|
| Module missing in layer | Dependency not in include array | Add to layer include; or let exclude chain handle it |
| Module duplicated in multiple layers | Not excluded from subsequent layers | Add to exclude array of all layers that come after |
| NLS strings not working after build | localeList missing locales | Add all locales to localeList in profile |
| Templates not loading | internStrings not set | Set internStrings: true in profile |
| Closure fails with "variable undeclared" | Global variable access | Use explicit AMD dependencies; remove global dojo.* calls |
CSS @import not inlined | cssOptimize not set | Add cssOptimize: "comments" to profile |
// Check what's in a built layer — look for specific module
grep -c "define(" dist/myapp/main.js # number of AMD defines in the file
grep "myapp/views/Dashboard" dist/myapp/main.js # confirm module is present
// isDebug=true in production build — verbose loader logging
// Only use temporarily for debugging!
var dojoConfig = {
isDebug: true,
parseOnLoad: false
};
// After build: verify layer loading in DevTools Network tab
// You should see only your 2-3 layer files, not individual modules
// If you still see 100+ requests, something is wrong with the build
Create a complete build profile for the Phase 4 CRUD dashboard that produces:
dist/ with Closure simple optimization// profiles/dashboard.profile.js
var profile = (function() {
return {
releaseDir: "dist",
action: "release",
layerOptimize: "closure",
optimize: "closure",
cssOptimize: "comments",
internStrings: true,
localeList: "en-us,fr",
packages: [
{ name: "dojo", location: "dojo" },
{ name: "dijit", location: "dijit" },
{ name: "dojox", location: "dojox" },
{ name: "myapp", location: "myapp" }
],
layers: {
"dojo/dojo": {
include: [
"dojo/_base/declare", "dojo/_base/lang", "dojo/_base/array",
"dojo/dom", "dojo/dom-class", "dojo/dom-style", "dojo/dom-construct",
"dojo/dom-attr", "dojo/on", "dojo/query", "dojo/topic",
"dojo/when", "dojo/Deferred", "dojo/promise/all",
"dojo/request", "dojo/store/Memory", "dojo/store/Observable",
"dojo/Stateful", "dojo/i18n",
"dojo/i18n!myapp/nls/messages"
],
boot: true,
customBase: false
},
"myapp/dijit-layer": {
include: [
"dijit/_Widget", "dijit/_TemplatedMixin", "dijit/_WidgetsInTemplateMixin",
"dijit/form/ValidationTextBox", "dijit/form/Select",
"dijit/form/DateTextBox", "dijit/form/Button",
"dijit/form/CheckBox", "dijit/form/Form",
"dijit/Dialog",
"dijit/layout/BorderContainer", "dijit/layout/TabContainer",
"dijit/layout/ContentPane"
],
exclude: ["dojo/dojo"]
},
"myapp/main": {
include: [
"myapp/App",
"myapp/views/Dashboard",
"myapp/views/EmployeeList",
"myapp/views/EditForm",
"myapp/topics"
],
exclude: ["dojo/dojo", "myapp/dijit-layer"]
}
}
};
})();
// Build command:
// node util/buildscripts/build.js profile=profiles/dashboard action=release
// Production HTML — load 3 scripts instead of 200+:
// <script src="dist/dojo/dojo.js"></script>
// <script src="dist/myapp/dijit-layer.js"></script>
// <script src="dist/myapp/main.js"></script>
// <script>require(["myapp/App"]);</script>
| Concept | Key Rule |
|---|---|
| Build command | node util/buildscripts/build.js profile=myapp action=release |
layerOptimize | Use "closure" for production; false for debug builds |
cssOptimize | "comments" inlines @import and strips comments |
internStrings | Always true in production — inlines dojo/text! templates |
customBase: true | Strips unused dojo/_base — only include what you explicitly list |
exclude in layers | Prevents modules from being duplicated across multiple layers |
boot: true | The layer acts as the loader bootstrap — this is your main entry script |
localeList | Must include all locales your app supports — else NLS fails after build |
| CDN vs local | CDN for prototypes only. Production always uses local build output. |
| Debug builds | optimize=false layerOptimize=false — readable output for troubleshooting |
Time: 6–7 hrs
Pages: 16–20
Exercises: 1
Build patterns: 4