Part 4-7: Project Setup and Detailed Measurements

This chapter documents the full setup, methodology, and raw measurements behind the comparisons throughout Part 4. It is the reference layer — the summaries in Part 4-1 through 4-6 draw from the data here.

The source is public: the applications live in zkoss-demo/web-ui-framework-comparison, Level 1 under apps/, Level 2 under apps2/ and Level 3 under apps3/. The underlying measurement reports — including the exclusions, the raw per-iteration samples, and the caveats each figure carries — are in that repository’s doc/ directory: measurement-report.md for Level 1, with separate reports for Levels 2 and 3.

Test environment — Level 1 (Employee Manager)

Level 1 ran all six implementations on Spring Boot 3.3.4 with the Jakarta namespace, under a single multi-module Maven project. A shared Maven module — 7 files, 398 lines of entities, repositories, services, and seed data — provided the backend for every framework, including Vaadin. Each framework implementation is only the UI layer. Seed data was 8 departments and 57 employees, 4 of them inactive.

Hardware and toolchain: Apple M1 Pro (8 cores), macOS 15.7.3, Zulu JDK 17.0.4.1, Maven 3.9.1. All builds were run offline (mvn -o) to remove network variance. Framework versions: ZK 10.3.0.1 CE (zul — no EE component is used anywhere in the application) with zkspringboot 3.2.7.1, Vaadin 24.4.5, Wicket 10.1.0, React 18.3.1 with Vite 5.4.21, Angular 17.3.12. ZK runs on Jetty 12.0.13, which zkspringboot pulls in; the other five run on Tomcat, as their starters default.

All six applications render 15 rows on the first page of the employee list, verified in source and in the rendered DOM.

Test environment — Level 2 (HR Workspace)

Level 2 standardized all six implementations on Spring Boot 3.3.4 with the Jakarta namespace, under a single multi-module Maven project. The backend was no longer shared at Level 2 — each framework provided its own data layer, since Level 2 focused on frontend component richness rather than backend integration patterns.

React and Angular were standalone frontend applications at Level 2 — no Spring Boot backend. They used hardcoded mock data. This reflects realistic Level 2 frontend-focused development, and is noted in the measurements.

Test environment — Level 3 (Live HR Operations Center)

Level 3 used Spring Boot 3.3.4 with the Jakarta namespace (apps3/), reusing the Level 2 shared module as a Maven dependency. Only ZK and Vaadin provided full implementations. The other four frameworks provided explanation cards documenting the implementation approach and estimated effort.

All measurements were taken on the same hardware under consistent conditions, with JDK 17 and clean builds (tests skipped). Level 1 response times are measured in a real browser and are described in the Level 1 section below. Level 2 and Level 3 response times are warm server averages (requests 2–5 after one initial warm-up request).

Level 1: Employee Manager — Full Measurements

Lines of code

Counted with wc -l over hand-written source. The shared backend module (7 files, 398 lines) is counted once and excluded from per-framework totals. Stylesheets are excluded from every framework on the same rule — Vaadin 0, Thymeleaf 34, Wicket 75, Angular 137, ZK 315, React 575 lines — so that no total includes styling.

Framework UI Lines Backend Lines Total JS Written
Vaadin 652 (Java only) — (shared) 652 0
ZK 402 (Java ViewModel) + 377 (ZUL) — (shared) 779 0
Wicket 612 (Java) + 319 (HTML) — (shared) 931 0
Thymeleaf 353 (Java) + 651 (HTML) — (shared) 1,004 0
React 730 (JSX) + 307 (Java API) 307 1,037 730
Angular 810 (TS) + 295 (Java API) 295 1,105 810

Two things have to be read alongside this table.

Vaadin is the smallest, and ZK is 19.5% above it — 652 lines against 779. Most of that 127-line difference is structural: Vaadin composes its UI in Java with no template language, so where ZK writes a ViewModel and a ZUL template per view, Vaadin writes a single Java class. Vaadin also inherits Vaadin’s Lumo theme, adding only six getStyle() chains, where the other five hand-write the design in CSS. That styling work is excluded from every total by the rule above, so it does not show up in these figures — but it does mean Vaadin’s 652 lines buy a themed look rather than the custom one the others build.

ZK pages in memory; the other five page on the server. The user-facing behavior is identical — same pagination, same sorting — but the paging happens in a different place. The other five issue a bounded query per page turn, so their controllers hold page state and implement a sort callback. ZK loads the whole result set into a ListModelList and lets the listbox page and sort it in memory, so its ViewModel needs neither. Part of that saving is the framework doing the work for you; part of it is an unbounded query and a model held per user session — free at 57 rows and idiomatic at that size, but a cost that grows with the dataset. This is a choice made for this application, not a framework limit: ZK supports server-side paging through a custom ListModel.

Build times

Warm mvn -o clean package -DskipTests -pl <module>; each module built twice, the second run reported. Frontend builds were measured separately with a warm cache.

Framework Maven Frontend build Full build
ZK 1.71s 1.71s
Thymeleaf 2.36s 2.36s
Wicket 2.71s 2.71s
React 2.42s 1.43s (Vite, 91 modules) 3.85s
Vaadin 5.80s — (runs in-Maven) 5.80s
Angular 2.56s 5.69s (ng build) 8.25s

ZK builds fastest of the six. Part of that advantage is dependency count: on CE there are five fewer jars to resolve and repackage than on EE. Vaadin’s Maven plugin drives its frontend toolchain inside the Maven run, which is why its single figure is larger than the other server-side frameworks’. Both frontend builds are reproducible — re-running Vite and ng build produced byte-identical bundles with identical content hashes.

Deployable artifact size

Framework JAR Size Frontend JS Frontend CSS Notes
React 46.7 MB 214.6 KB (72.2 KB gzip) 6.9 KB Smallest artifact
Angular 46.8 MB 309.3 KB (83.2 KB gzip) 6.6 KB  
Thymeleaf 48.2 MB 78.8 KB (Bootstrap, CDN) 323.4 KB (CDN) Server-rendered, but the CDN adds 402 KB to first load
Wicket 50.7 MB 0 0 (inline) Links nothing at all
ZK 62.4 MB 1,337.3 KB 382.2 KB Framework payload cached after first load
Vaadin 88.5 MB 2,816.9 KB 0 (in JS) Largest on both dimensions

Sizes in KB = 1,024 bytes as measured over HTTP; build tools report decimal kB, so Vite’s “219.71 kB” is the same file this table calls 214.6 KB.

The ZK row is ZK CE (zul), which is all this application needs — it uses no EE component. Building it against zkmax instead pulls in five more jars and gives a 68.7 MB artifact with 1,520 KB of first-load JavaScript and 543 KB of CSS. ZK’s 382 KB of CSS is mostly framework theme, not the application’s own 315-line stylesheet.

Time until rows appear on screen

Earlier revisions of this chapter reported server-side request timings. That metric has been retired: it compared unlike things — a REST endpoint returning JSON against a framework rendering a complete page — and it could not measure Vaadin at all. It is replaced by what the user actually waits for.

The metric is the time until the user sees rows, measured in a real browser with Playwright 1.62.1 and headless Chromium. The milestone is identical for all six applications: 15 distinct employee e-mail addresses present in the document, caught by a MutationObserver, then one requestAnimationFrame for the paint. Every application renders an e-mail column, so this counts data rows without depending on any framework’s markup — ZK’s tr.z-listitem, Vaadin’s shadow-DOM grid rows and four plain <tbody> tables are all counted by one rule. No landing page contains an e-mail address, so the milestone cannot fire early.

Two scenarios were timed. A warm click starts when the click lands in an already-open application with its asset cache primed. A cold visit starts at navigation start in a fresh browser context with an empty cache. Both use absolute epoch times rather than per-document clocks, so a click that triggers a full-page navigation — Thymeleaf and Wicket — is still measured across the document boundary.

Framework Warm click Cold visit
Thymeleaf 22.7 ms 67.7 ms
Wicket 39.4 ms 72.5 ms
Angular 39.6 ms 63.4 ms
React 52.5 ms 38.5 ms
ZK ~93 ms 99.9 ms †
Vaadin 201.0 ms 663.8 ms

† ZK’s cold visit loads /employee-list.zul, a bare fragment with no header or sidebar. It is not comparable to the other five, which load their full shell. ZK’s shell load to a clickable navigation control was measured separately at 153.9 ms.

What this data supports. ZK puts rows on screen more than twice as fast as Vaadin — 93 ms against 201 ms on a warm click, and Vaadin is the slowest of the six on both scenarios. Against the other four, ZK loses: it finishes fifth of six on the warm click, ahead only of Vaadin. Thymeleaf reaches rows in roughly a quarter of ZK’s time because its client does almost nothing once the HTML arrives, where ZK’s browser has to execute script, build widgets, lay them out and paint.

Caveats that travel with these numbers.

  • n=3, preliminary. Three timed iterations per application after a discarded warm-up, mean reported, fresh browser context per iteration. Run-to-run spread on the same application reached about 20%. ZK’s warm click is 108.9 ms over 3 iterations and 90.4 ms over 20, which is why 93 ms is the figure used.
  • Loopback only. The cold-visit column does not charge ZK’s 1.7 MB or Vaadin’s 2.8 MB of framework JavaScript a realistic transfer cost, so it flatters both.
  • C1-only JIT. Every application was started the documented way, mvn spring-boot:run, which forks the JVM with -XX:TieredStopAtLevel=1. C2 never engages, so no application here reaches peak JIT performance. This is uniform across the six, so the comparisons stand, but every absolute millisecond is pessimistic against a production java -jar launch.

Architecture complexity

Framework Layers State Languages
Vaadin 1 Server session Java only
ZK 2 Server session Java + ZUL
Thymeleaf 2 Stateless Java + HTML
Wicket 2 Server session Java + HTML
React 3 Client (useState) Java + JSX
Angular 4 RxJS + services Java + TypeScript

Level 2: HR Workspace — Full Measurements

Component implementation status

Each view is classified by how the component was delivered:

  • ✅ Native — built-in component from the framework vendor, no external library
  • 🔷 Ecosystem wrapper — official framework-specific package (e.g. @fullcalendar/react, PrimeReact, PrimeNG)
  • 🔶 Directory add-on or CDN — Vaadin Directory add-on (free, Java API), or CDN-loaded JS library (correct pattern for server-side frameworks with no component library)
  • 💰 Commercial placeholder — paid component available but not implemented; placeholder shown
  • ⚠️ No solution — no framework-appropriate solution found at any tier
View ZK Vaadin Thymeleaf Wicket React Angular
Event Calendar <calendars> (native EE) 🔶 FullCalendar for Flow (Directory, FlowingCode) 🔶 FullCalendar CDN 🔶 FullCalendar CDN via renderHead 🔷 @fullcalendar/react 🔷 @fullcalendar/angular
Org Chart <organigram> (native EE) 🔶 OrgChart Add-on (Directory, FlowingCode) 🔶 OrgChart.js CDN 🔶 OrgChart.js CDN via renderHead 🔷 PrimeReact OrganizationChart (MIT) 🔷 PrimeNG p-organizationChart (MIT)
Salary Pivot <pivottable> (native EE) 🔶 PivotTable for Vaadin (Directory, Vaadin Component Factory) 🔶 PivotTable.js CDN 🔶 PivotTable.js CDN via renderHead 🔷 react-pivottable (MIT) 💰 Syncfusion / DevExtreme (commercial — placeholder)
Portal Dashboard <portallayout> + <charts> (native EE) ✅ Vaadin Dashboard + Charts (Vaadin Pro, commercial) 🔶 Gridstack.js + Chart.js CDN 🔶 Gridstack.js + Chart.js CDN via renderHead 🔷 react-grid-layout + recharts (MIT) 🔷 angular-gridster2 + ng2-charts (MIT)
Views delivered 4/4 4/4 4/4 4/4 4/4 3/4
Native framework components 4 1 (Dashboard + Charts, commercial) 0 0 0 0
Ecosystem wrappers 4 3
Directory add-ons / CDN 3 (Directory add-ons, free Java API) 4 (CDN) 4 (CDN)
Commercial placeholder 1 (Pivot)

Key notes on Vaadin: The three Directory add-ons (FullCalendar for Flow, OrgChart Add-on, PivotTable for Vaadin) expose Java APIs — the developer writes no JavaScript. They come from third-party vendors on the Vaadin Directory (FlowingCode, Vaadin Component Factory), not from Vaadin Ltd directly. Each has its own release schedule separate from Vaadin core.

Key notes on Angular: No free native Angular pivot table component exists. Syncfusion and DevExtreme both offer commercial solutions. The Angular implementation shows a placeholder for this view.

Lines of code and JavaScript written

Framework Total LOC JS Written Views Delivered Notes
ZK 430 0 4/4 All native ZK components; Java ViewModel + ZUL tags
Vaadin 592 0 4/4 Directory add-ons use Java API — no JS strings required
Thymeleaf 274 ~80 (inline <script> tags) 4/4 CDN lib init code in HTML templates
Wicket 337 ~100 (Java string-embedded) 4/4 JS embedded in OnDomReadyHeaderItem Java strings
React ~265 ~265 (JSX) 4/4 FullCalendar, PrimeReact OrgChart, react-pivottable, react-grid-layout + recharts
Angular ~410 ~410 (TypeScript) 3/4 FullCalendar, PrimeNG OrgChart, angular-gridster2 + ng2-charts; Pivot = placeholder

Third-party libraries required

Framework Libraries Needed Integration Pattern
ZK 0 All native framework components
Vaadin 3 Directory add-ons + 1 native commercial Java API (no JS); from 3 different vendors
Thymeleaf 4 (FullCalendar, OrgChart.js, PivotTable.js, Gridstack.js + Chart.js) CDN <script> tags
Wicket 4 (same as Thymeleaf) CDN via JavaScriptHeaderItem.forUrl() + renderHead
React 5–6 NPM packages (from 4 vendors) Framework-native wrappers (@fullcalendar/react, primereact, react-pivottable, react-grid-layout, recharts)
Angular 4–5 NPM packages (from 3–4 vendors) Framework-native wrappers; Pivot requires commercial license

Build times and artifact sizes (Level 2)

Framework Build Time JAR Size Frontend JS Notes
Thymeleaf ~2.1s 47 MB CDN only Minimal deps
Wicket ~2.2s 49 MB CDN only Wicket framework bundled
React ~2.1s† N/A† 920 KB (275 KB gzip) Standalone frontend; FullCalendar, react-pivottable, react-grid-layout
ZK ~3.5s 76 MB ~1.5 MB (ZK + ZKCharts) Larger than Level 1 due to zkcharts and zkmax JARs
Angular ~6.3s† N/A† 756 KB (202 KB gzip) Standalone frontend; Pivot = commercial placeholder
Vaadin ~6.9s 89 MB ~2.8 MB Includes Highcharts, Lit/Polymer; slowest build

†Standalone frontend build — no Spring Boot backend included in this measurement.

Server response times — Level 2 (warm, localhost, avg of requests 2–5)

Framework Endpoint Avg Response Time Notes
React GET / (Vite dev) ~1–2 ms Static HTML shell
Angular GET / (ng serve) ~1–2 ms Static HTML shell
Thymeleaf GET /calendar ~2–3 ms Server-rendered, CDN component init
Wicket GET /calendar ~2 ms CDN component init
ZK GET /calendar.zul ~7–9 ms Full calendar component render + session init
Vaadin GET /calendar ~7–8 ms Vaadin AJAX-based navigation

JavaScript written by developer — Level 2

The Level 2 JS-written metric is the sharpest differentiator between frameworks at this complexity level:

Framework JS/TS Written Source
ZK 0 lines All 4 views use native ZK components
Vaadin 0 lines Directory add-ons expose Java API — no JS required
Thymeleaf ~80 lines Inline <script> blocks initializing CDN libraries
Wicket ~100 lines JS embedded in Java string literals via renderHead
React ~265 lines All UI logic in JSX across 4/4 real npm components
Angular ~410 lines TypeScript across 3/4 real components; Pivot = placeholder

ZK and Vaadin both achieve zero JavaScript at Level 2. The distinction between them is who owns the components: ZK’s are all from Potix Corporation, Vaadin’s are from Vaadin Ltd (Dashboard) plus three third-party Directory vendors.

Level 3: Live HR Operations Center — Full Measurements

Scope

Two features were fully implemented in ZK and Vaadin. React, Angular, Thymeleaf, and Wicket ran as minimal Spring Boot applications but showed placeholder views — styled panels rendered within the app in place of each unimplemented feature, describing the libraries, infrastructure, and developer effort that would be required.

Feature 1: Large Dataset Grid (10,000 rows)

Metric ZK Vaadin React Angular Thymeleaf Wicket
Built-in virtualization ✅ ROD (1 XML attribute) ✅ DataProvider.fromCallbacks() ❌ External library required ⚠️ CDK Virtual Scroll (built-in, needs wiring) ❌ None ⚠️ Pagination only
DOM nodes at runtime ~20 (viewport only) ~50–100 (page window) Library-dependent Library-dependent All rows Current page
Server round-trip on scroll None (data in Java heap) Yes (offset/limit query per page) N/A N/A N/A N/A
Developer UI code 64 lines (32 VM + 32 ZUL) 50 lines ~200 LOC JS + library ~150 LOC TS + CDK wiring ~200 LOC JS ISortableDataProvider impl
JavaScript required 0 0 ~200 ~150 ~200 0 (pagination only)

ZK note: For datasets too large for Java heap, ZK also provides BigListBox + MatrixModel (ZK EE), where Java heap usage stays constant regardless of dataset size — only the visible rows occupy memory.

Vaadin note: DataProvider.fromCallbacks() makes a database query per scroll page. Developer writes two lambda callbacks; Vaadin handles the UI-side virtualization.

Angular note: Angular CDK Virtual Scroll is built-in, which is an advantage over React (external library). However, the developer must wire it explicitly: itemSize strategy, trackBy functions, viewport connection.

Wicket/React/Angular/Thymeleaf note: LOC estimated — these frameworks were not fully implemented at Level 3. The LOC represents a reasonable implementation scope based on the integration approach described, not measured code.

Feature 2: Real-Time Server Push Dashboard

Metric ZK Vaadin React Angular Thymeleaf Wicket
Built-in mechanism ✅ EventQueue (APPLICATION scope) ✅ @Push + UI.access() ❌ None ❌ None ❌ None ⚠️ AbstractAjaxTimerBehavior (polling)
Transport WebSocket/Comet (automatic) WebSocket (via @Push) STOMP/SockJS or native WS RxJS WebSocket SSE/WebSocket Interval polling
Thread safety Automatic ui.access() required by developer Developer-managed Developer-managed Developer-managed N/A
Developer push code ~15 lines ~30 lines ~350 lines ~300 lines ~400 lines ~50 lines (polling)
JavaScript required 0 0 ~350 ~300 ~400 0

ZK note: APPLICATION-scoped EventQueue broadcasts to all connected sessions from a single publisher. Developer subscribes in ViewModel; thread safety is handled by ZK automatically.

Vaadin note: @Push must be on AppShellConfigurator (not AppLayout — placing it there causes a startup RuntimeException). All UI updates must be wrapped in ui.access() to ensure thread safety.

Wicket note: AbstractAjaxTimerBehavior is client-initiated polling, not true server push. Suitable for low-frequency updates; not suitable for applications requiring immediate push delivery. The LOC represents a reasonable implementation scope based on the integration approach described, not measured code.

React/Angular/Thymeleaf note: Achieving real-time push requires assembling WebSocket infrastructure on both server and client sides. None of these frameworks provide a built-in mechanism. The LOC represents a reasonable implementation scope based on the integration approach described, not measured code.

Total UI LOC and JavaScript written — Level 3

Framework Grid LOC Push LOC Total UI LOC JS Required
ZK 64 (VM + ZUL) 72 (VM + ZUL) 136 0
Vaadin 50 110 160 0
React N/A (explanation card) N/A Library + integration required
Angular N/A (explanation card) N/A CDK + integration required
Thymeleaf N/A (explanation card) N/A Library + JS infrastructure required
Wicket N/A (explanation card) N/A 0 (polling only)

Level 4: Enterprise Requirements — WCAG Accessibility

Accessibility support by framework

Framework Built-in Module Coverage Manual Effort Overall Effort
Vaadin ✅ Core (always on) Very strong — standard components WCAG 2.1 AA Moderate Low
ZK ✅ za11y.jar Strong Moderate Low–Medium
Angular ✅ @angular/cdk/a11y Moderate Moderate Medium
React ⚠️ Third-party only Library-dependent Moderate–Extensive Medium–Very High
Wicket ❌ None Weak Extensive High
Thymeleaf ❌ None None Everything Very High

Framework-level accessibility detail

Vaadin — Accessibility is built into every component, not a separate jar. Standard components are WCAG 2.1 AA compliant and audited annually by TetraLogical. No separate configuration is required. Remaining manual work is limited to application-level semantics (page landmarks, form labels where context is custom, contrast in Lumo overrides).

ZK — Adding za11y.jar to the classpath retrofits WAI-ARIA roles, keyboard navigation, screen reader live regions, and high-contrast support across all ZK components. Some manual work required for application-specific patterns not covered by the module.

Angular — The @angular/cdk/a11y package provides LiveAnnouncer, FocusTrap, ListKeyManager, and high-contrast detection. These are useful building blocks, but component libraries (PrimeNG, Angular Material) have inconsistent accessibility quality. More manual testing and remediation than ZK or Vaadin.

React — No built-in accessibility support. The best path is choosing an accessible component library (Radix UI, Adobe React Spectrum, Headless UI), but coverage depends entirely on library choice and version. High variability.

Wicket — No accessibility module. All ARIA roles, labels, keyboard navigation, and focus management must be written explicitly by the developer. High effort for full WCAG compliance.

Thymeleaf — A template engine with no component model and no accessibility scaffolding. Every semantic landmark, ARIA attribute, keyboard handler, and focus behavior is the developer’s responsibility.

Security and enterprise support summary

Framework Commercial Support Security Process Long-term Assurance
ZK ✅ Potix Corporation Snyk + CodeQL + SonarQube CI/CD; ISO 27001; OSCP pen testing Commercial — direct financial incentive
Vaadin ✅ Vaadin Ltd Internal security practices; formal audit program Commercial — direct financial incentive
React ❌ Community only Meta internal practices; community CVE reporting Meta-backed; ecosystem dependencies not guaranteed
Angular ❌ Community only Google internal practices; community CVE reporting Google-backed; ecosystem dependencies not guaranteed
Thymeleaf ❌ Community only Community CVE reporting Apache/community maintained
Wicket ❌ Community only Community CVE reporting Apache maintained — slow but stable

Notes on methodology

Level 1 response times are browser measurements to a shared on-screen milestone, described in the Level 1 section above; they are n=3 and preliminary. Level 2 and Level 3 response times were measured warm — after one initial request to allow JVM JIT compilation and connection pool initialization — and represent server processing time at localhost, with network latency excluded.

Line counts include all developer-written code in the application layer. Generated code, framework internals, stylesheets, build configuration, and the shared backend module are excluded from per-framework totals.

For Level 2, JavaScript embedded inside Java string literals (as in Wicket’s renderHead pattern) is counted as JavaScript written by the developer — it is application code, regardless of the surrounding language.

The Level 2 measurements reflect fully functional implementations wherever a framework-appropriate solution was available. Where a commercial placeholder was used (Angular’s pivot table), it is noted and excluded from line counts.

The React and Angular Level 2 applications were built as standalone frontend applications with no Spring Boot backend, using hardcoded mock data. This reflects how these frameworks are typically developed when the backend is separate — the frontend is a self-contained application.

Level 3 line counts for React, Angular, and Thymeleaf are estimates only — these frameworks were not implemented, and no code was written or measured. The figures in the Feature 1 and Feature 2 tables reflect the expected scope of a complete implementation based on the documented integration patterns, not actual measurements.