# What a PX page actually weighs, and the switch that halves it

> A PX view ships about 2.3 MB of framework JavaScript before your graphic. Gzip is off by default, and /module/* bypasses the filter that would do it.

Source: https://plantroomlabs.com/notes/px-page-payload-and-gzip/  
Published: 2026-09-27 (27 September 2026) · Plantroom Labs  
Topics: PX graphics, JACE, Performance

Every ux or PX-in-a-browser session loads 718 KB of framework before it draws anything, and a stock station sends all of it uncompressed. Both numbers are readable from the jars, without a station.

## The payload is declared in Java, so it can be counted

"Can the controller serve this graphic?" is normally answered with an opinion — it depends, keep it simple, use a Supervisor. It has a measurable form: when an operator opens the view, how many files and how many bytes does the station send, and how much of that is your drawing?

That is answerable without a running station, because the framework declares its own client-side dependency graph in Java. Every module that ships browser code registers a `BJsBuild` singleton naming exactly one pre-built bundle plus the builds it depends on, and the page writer resolves that set into the nested `require()` calls it writes into the page. Read the classes and you have the payload the framework will ask for, per view, exactly as the framework computes it. The figures below come from a stock Niagara 4.15 install.

## The floor: 718 KB before your graphic exists

Every session loads the same bootstrap, whatever the view contains. The requirejs alias table decides which file each name resolves to, and at stock defaults it resolves to the minified copy of each library:

| File | Raw | Gzipped |
|---|---|---|
| require.min.js | 17.2 KB | 6.4 KB |
| js.built.min.js (20 AMD modules) | 50.7 KB | 23.7 KB |
| jquery.min.js | 85.5 KB | 29.7 KB |
| bluebird.min.js | 79.6 KB | 22.8 KB |
| handlebars.min.js | 87.3 KB | 26.6 KB |
| underscore.min.js | 19.2 KB | 7.4 KB |
| hbs.built.min.js | 7.2 KB | 3.2 KB |
| lex, log and css plugins | 8.7 KB | 3.9 KB |
| baja.js (the BajaScript plugin) | 9.0 KB | 2.8 KB |
| bs.built.min.js (202 AMD modules) | 354.0 KB | 89.8 KB |
| **Total** | **718.4 KB** | **216.3 KB** |

Twelve files, 718 KB, 222 AMD modules, and not one pixel of anybody's building. BajaScript is the largest single item and it is not loaded by a `define([...])` list that a scan would find — the `baja!` plugin requires it at runtime, which is why it is missing from most estimates of this.

## What the view itself adds

Resolving each build's declared closure gives two honest figures rather than one. The *certain* column is the floor plus every bundle and stylesheet in the resolved graph: those are written into the page's `require()` blocks and fetched whether or not a particular widget is used. The *ceiling* adds every loose file a bundle member declares that no loaded bundle provides — the Ace code editor at 446.9 KB, the icon sprite stylesheet at 216.2 KB, moment, the dialogs plugin, jQuery contextMenu. Those are fetched lazily, when something that needs them is instantiated, so a real page lands between the two columns.

| View | Certain (raw / gzip) | Ceiling (raw / gzip) |
|---|---|---|
| A bare bajaux widget | 869 / 260 KB | 876 / 262 KB |
| PX served to a browser (hx container) | 1,871 / 507 KB | 2,816 / 707 KB |
| Graphics primitives (gx) | 1,994 / 530 KB | 2,870 / 713 KB |
| PX with the standard widget kit | 2,270 / 597 KB | 3,146 / 780 KB |
| A chart on the page | 2,585 / 682 KB | 3,735 / 956 KB |
| An analytics chart | 3,111 / 793 KB | 5,006 / 1,280 KB |

One bundle dominates all of them: the web editors bundle, 916 KB carrying 417 AMD modules — field editors, property sheet, nav tree, composite editor — pulled in by the graphics module, the standard widget kit, the chart module and the hx container alike. A read-only floorplan on a wall panel gets the whole editing toolkit, because the dependency is declared at build level rather than at view level.

Counted as AMD modules instead of files, a PX page with standard widgets is about 900 modules arriving in 31 requests. That ratio is the bundling doing its job, and it is the reason webdev mode belongs nowhere near a site station: with it on for a module, requirejs fetches that module's members individually and the same view goes from tens of requests to several hundred.

## Gzip is off by default, and it is one hidden slot

This is the finding worth acting on. The station's web server does have a gzip handler, and its MIME list covers JavaScript, CSS, HTML, plain text, JSON and the XML types — everything in the tables above. But the handler is only added to the handler collection when the web service's `gzipEnabled` property is true, and that property is declared `HIDDEN` with a default of **false**. You will not see it on the property sheet until you show hidden slots.

Nothing downstream compensates. Module resources are served from `/module/*` by a file servlet whose only content-encoding path requires the file on disk to already *be* a gzip file, which no bundle is. The heap-memory file cache that does compress is mapped to `/vfile/*` only.

> **Out of the box, a PX page ships about 2,270 KB of uncompressed JavaScript and CSS to every operator whose browser cache is cold. With the switch on, about 597 KB.** As arithmetic rather than measurement: on a 10 Mbit/s link that is 1.9 s of transfer against 0.5 s; on a 2 Mbit/s site uplink, 9.3 s against 2.4 s, per cold operator.

Two caveats, both checkable. The gzip handler *is* present in the compact3 build of the web server that ships in the controller's own distribution file, so this is not a Supervisor-only switch. And the trade is controller CPU: this path is not cached, so it compresses per response. Turn it on deliberately, having tested it on your own hardware, before doing it at a site where many operators log in cold at once.

## The second load is nearly free, and every upgrade makes it cold

Module files are long-term cacheable by construction: because they live in a module's space, the servlet sends `Cache-Control: private, must-revalidate, max-age=2592000` — thirty days. The second graphic an operator opens costs almost nothing, which is the right design.

It has a consequence people meet without recognising it. The page's requirejs config carries a `urlArgs` cache-buster derived from the last build time, so after a station upgrade or a module install every operator's first graphic is a full cold load again. If that is 2.3 MB uncompressed per operator, the morning after a Tuesday-night upgrade is when the site link gets measured for you.

Caching also switches off in two places that are easy to confuse with webdev: the servlet stops sending those headers if `niagara.web.addExpires` is set to false, or if its own file-servlet webdev flag is enabled. That is a different switch from the per-module one that makes requirejs fetch loose files.

## The controller has no image pipeline at all

The other half of a graphics budget is the artwork, and here the platform decides for you. The controller platform's distribution files name their VM as a compact3 ARM build, and that JVM's `rt.jar` can be counted: it carries **zero** classes under `java/awt`, `javax/swing`, `javax/imageio` and `java/beans`. The 64-bit JRE shipped in the same install carries 599, 1,951 and 89 respectively.

So nothing in a station on that platform can decode, scale, crop or recompress an image. There is no server-side thumbnail and no second chance: the render you engineer is the file every browser downloads, at the pixel dimensions and in the format you saved it. On a floorplan job that single fact usually outweighs every JavaScript figure above, because a render exported at 3000 px wide is a multi-megabyte PNG and the controller will not help you.

## What to do with these numbers

- **Budget the framework, not just the drawing.** The drawing is usually the small half.
- **Turn the gzip switch on deliberately**, tested on the controller, and check it again after an upgrade.
- **Size the image once, correctly**, in whatever rendered it.
- **A chart is not free.** Putting one on a landing view adds the chart and history bundles to every operator's first load of that page.
- **Reuse view types.** Payload is per view type and the browser cache is per file, so five graphics built from the same widget kit pay for the kit once.
- **If a view genuinely has to be light** — a wall panel, a phone over 4G, a guard's kiosk — the framework floor is the argument for serving a static page from the station's file space and keeping the station as the data source.
For contrast, the three [live demos on this site](https://plantroomlabs.com/work/) are one self-contained file each, between 9.9 KB and 19.3 KB, one request, no framework floor — which is not a replacement for PX or bajaux, because they are not editable in Workbench and have no component binding. It is the measurement of what the alternative weighs. The artwork side of the same question is in the note on [3D floorplan graphics](https://plantroomlabs.com/notes/3d-floorplan-graphics-in-niagara/), what else the controller is carrying is in [what actually runs on a JACE](https://plantroomlabs.com/notes/what-runs-on-a-jace/), and the sheets themselves are [PX graphics work](https://plantroomlabs.com/services/px-graphics/) or a [bajaux widget](https://plantroomlabs.com/services/bajaux-widgets/) when no stock widget will do.
