Performance & SEO
Most of this is on by default — pages ship one small cached runtime instead of a framework bundle, and the server does the rendering.
SEO Metadata
One builder emits the title, description, canonical link, Open Graph, and Twitter card tags — all consistent from a single declaration.
head(
metaCharset(), metaViewport(),
Seo.of("JWeb — Java Web Framework",
"Build complete web apps entirely in Java")
.url("https://jweb.dev/")
.image("https://jweb.dev/og.png")
.siteName("JWeb")
.render()
)Image Optimization
Images are resized on the fly and cached immutably — no build step, no dependencies (ImageIO).
img("/jweb/img?src=/static/hero.jpg&w=800")- src is restricted to classpath static/ and public/ resources
- w resizes to that width, keeping the aspect ratio (max 3840)
- Results are cached in memory and served with a 1-year immutable header
Asset Caching
The client runtime and prefetch script are served as external files with a content-hash version, so browsers cache them across every navigation and only the page HTML travels on each request.
<script src="/jweb/runtime.js?v=67f7c91e"></script>
Cache-Control: max-age=31536000, public, immutableStreaming & Prefetch
- Streamed.of(() -> page) flushes the shell immediately and streams Suspense blocks as their data resolves (see Streaming SSR)
- Link prefetching warms pages on hover, so navigation feels instant
- A background warmup pre-renders registered pages at startup, so the first visitor doesn't pay lazy-initialization cost
- gzip compression is enabled for text responses
Secure Baseline
One call adds the production essentials — security headers, request IDs, and compression negotiation — to every route:
app.use(Middlewares.recommended());Request IDs, Logs and Traces
Every request has an id before anything sees it: the caller's X-Request-ID or a fresh one. The response carries it back, every log line of the request shows it, the error page quotes it. jweb.Trace opens a span around every route, SQL statement, job, PubSub delivery, mail, Fetch and browser event; add the actuator and they become Micrometer observations, add micrometer-tracing and they are spans in the request's trace.
import jweb.Trace;
import jweb.Middlewares;
record Order(long id) {}
Order order = new Order(9);
String id = req.id(); // the same id on the response, in the logs, on the error page
app.use(Middlewares.logging()); // GET /orders/9 200 12ms
try (Trace.Span span = Trace.span("order.price", "kind", "retail")) { // tags at the start: low cardinality
span.tag("order.id", order.id()); // tags later: high cardinality
}
Order priced = Trace.in("order.price", () -> order);