PHASE 9

Build System & Optimization

Bundle 200 AMD modules into 2 HTTP requests — build profiles, layers, closure compiler, CSS optimization, and production-ready deployment

Build Profile Layers Closure customBase internStrings Production
The payoff: A typical Dojo app without a build makes 150–400 separate HTTP requests on first load. After building with layers, that becomes 3–5 requests. On a 50ms-latency connection, that's the difference between a 3-second and 8-second load time.
1

Why Build? The HTTP Request Problem

Each 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.

Un-built app — browser network tab: dojo.js → 1 request dojo/_base/declare.js → 1 request dojo/on.js → 1 request dijit/_Widget.js → 1 request dijit/_TemplatedMixin.js → 1 request myapp/App.js → 1 request myapp/views/List.js → 1 request ... 193 more requests → 193 requests ───────────────────────────────────────────────── Total: ~200 requests | 2.4MB | 8.3s on 4G Built app — browser network tab: dojo.js → 1 request (Dojo core layer, 180KB gz) app-layer.js → 1 request (your app + dijit, 220KB gz) app.css → 1 request (inlined CSS, 45KB gz) ───────────────────────────────────────────────── Total: 3 requests | 445KB gz | 1.2s on 4G

The build tool concatenates all required modules into "layers" — single JS files that the loader reads from cache instead of fetching individually.

2

Build System Setup

# 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/)
⚠ Java required for Closure Compiler
The 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.
3

Build Profile Structure

// 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 }
    ]

  };
})();
4

Layers — Bundling Modules

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"
    ]
  }
}

Layer Strategy Patterns

PatternLayersBest For
Single bundle1 layer with everythingSmall apps, intranet, cache-able
Core + Appdojo-core layer + app layerMost enterprise apps — dojo core cached across pages
Core + UI + App3 layers as aboveLarge apps with many pages sharing same Dijit set
Per-page layers1 layer per major page/viewMulti-page apps where views have very different deps
5

JS Optimization: shrinksafe vs closure

shrinksafeclosure (SIMPLE_OPTIMIZATIONS)closure (ADVANCED_OPTIMIZATIONS)
Java requiredNoYesYes
Output size~60% reduction~70% reduction~80% reduction
RiskVery lowLowHigh — can rename public APIs
RecommendedWhen Java unavailable✅ Standard choiceOnly 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

Typical Build Output Sizes

Employee Dashboard App — before and after build
Un-built — 200 separate files
~2.4MB total (uncompressed) | ~200 HTTP requests
Built + shrinksafe — 3 files
~900KB (uncompressed) | 3 HTTP requests
Built + closure + gzip — 3 files
~445KB gzipped | 3 HTTP requests
6

CSS Optimization

// 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

Referencing CSS After Build

<!-- 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 -->
7

customBase — Strip Unused Dojo Core

By 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 — Feature Flags for Build

// 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
  }
};
8

internStrings — Inline Templates

Without 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
Production checklist for templates: Always use 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.
9

CDN vs Local — Production Decision

ScenarioUse CDN?Reason
Public website, light Dojo usageMaybeBrowser may have CDN version cached from another site
Enterprise intranet app�?� NoIntranet may not have internet access. Local = reliable.
Production app with build layers�?� NoCDN can't serve your custom layers. Must be local.
Quick prototype / demo✅ YesZero setup, no download needed
App needing offline support�?� NoCDN 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
10

Debugging Built Output

// 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

Common Build Problems & Fixes

ProblemCauseFix
Module missing in layerDependency not in include arrayAdd to layer include; or let exclude chain handle it
Module duplicated in multiple layersNot excluded from subsequent layersAdd to exclude array of all layers that come after
NLS strings not working after buildlocaleList missing localesAdd all locales to localeList in profile
Templates not loadinginternStrings not setSet internStrings: true in profile
Closure fails with "variable undeclared"Global variable accessUse explicit AMD dependencies; remove global dojo.* calls
CSS @import not inlinedcssOptimize not setAdd 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

🔧 Exercise 9.11 — Build the Phase 4 Dashboard App

Create a complete build profile for the Phase 4 CRUD dashboard that produces:

  • A dojo-core layer containing Dojo base + dojo/store + dojo/request
  • A dijit-ui layer containing all Dijit widgets used by the dashboard
  • An app layer containing your application code with inlined templates
  • Output to 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>

Phase 9 — Quick Reference

ConceptKey Rule
Build commandnode util/buildscripts/build.js profile=myapp action=release
layerOptimizeUse "closure" for production; false for debug builds
cssOptimize"comments" inlines @import and strips comments
internStringsAlways true in production — inlines dojo/text! templates
customBase: trueStrips unused dojo/_base — only include what you explicitly list
exclude in layersPrevents modules from being duplicated across multiple layers
boot: trueThe layer acts as the loader bootstrap — this is your main entry script
localeListMust include all locales your app supports — else NLS fails after build
CDN vs localCDN for prototypes only. Production always uses local build output.
Debug buildsoptimize=false layerOptimize=false — readable output for troubleshooting

Ready for the Capstone Apps?

All 9 phases complete. Now it's time to put everything together — three full production applications combining every concept from this tutorial.

Start Capstone 1: Enterprise HR Dashboard →