Taking control of WO's WebServerResources

October 26, 2025

Good ol' split install

WO has an interesting way of handling static resources. Traditionally, resources like images, CSS-files etc. are put into a project's WebServerResources folder, and on application deployment that folder gets copied to the webserver and served from there, bypassing the application entirely.

This "split install" made sense a while back since it freed the application from serving static resources, saving CPU cycles and bandwidth, more valuable resources at the time. Today however, serving static stuff is a comparatively lightweight task making split installs something of an unnecessary hassle.

So, for a while I've been skipping the split install and serving my webserver resources from my applications rather than the web server.

Why serve static resources through the application?

  1. It simplifies the build and deployment process
    You don't need the additional step of "installing" your webserver resources to the webserver. Deplyoment becomes just copying the application and restarting it.
  2. It simplifies the deployment environment
    With the webserver no longer serving any content directly, it becomes nothing more than a pure proxy for your app. This makes the deployment environment easier to understand and maintain.
  3. It ensures consistency between dev and production environments
    Resources are served the same way, using the same method and same code in all environments.
  4. You get application-level control over the serving of resources
    If you need to modify caching, operate on a resource before serving it etc. — everything is controlled from the application. You'll never have to mess with webserver configuration.
  5. It simplifies URLs and URL generation
    It sometimes feels like half of Wonder is hacks and tricks for locating static resources and generating the appropriate resource URLs. Serving resources through the application means URLs are simple, readable and uniform. And they're standard WO application URLs. No separate completely different looking "resource URLs".

Performance

Serving static resources from WO is fast. Fast enough that for my purposes, I don't even care exactly how fast, but for reassurance I did some quick and dirty benchmarking. The test is done on a ~230Kb resource on my own private website, hosted on a Hetzner CAX31 with -Xmx512M (not chosen specifically, just happens to be the heap size I've got set, should be fine with just about any value). Did the benchmarking locally using ab, speaking directly to a single instance of the application in production mode (meaning caching is enabled).

# Single request thread, 10.000 requests
# Completes in 6 seconds, ~0.6ms per request
# Transfer rate 375MB/sec
ab -n 10000 http://localhost:2001/Apps/WebObjects/Hugi.woa/res/app/ZillaSlab-Light.ttf

# 32 request threads, 10.000 requests
# Completes in 1.3 seconds, avg. ~4ms per request/~0.13ms across all concurrent requests
# Transfer rate 1703MB/sec
ab -c 32 -n 10000 http://localhost:2001/Apps/WebObjects/Hugi.woa/res/app/ZillaSlab-Light.ttf

This purely measures WO's request handling performance (no SSL, no network latency no mod_WebObjects etc). And yes, I'm fetching the same resource repeatedly so probably getting some performance benefits from that. But this is fast and would still be fast at even half the speed. And note I'm not using ab with -k (HTTP keep-alive) meaning each request initiates a new connection. Using keep-alive roughly doubles the performance in both cases.

How?

I've been doing something like this through a private framework for a while, but I recently started cleaning it up, making it more generic and added it to wonder-slim. The implementation can be found in ERXAppBasedResourceManager and ERXAppBasedResourceRequestHandler. Same code/method should work in any WO/Wonder app, it's simple and just means static resources are always served from your app using URLs that follow a simple format:

## URL format
.../App.woa/res/[frameworkName]/[resourceName]

## Example
.../App.woa/res/app/main.css

Work to do in slim's implementation

Even if already used in production, wonder-slim's implementation still needs some work:

  1. Webserver resources are cached in-memory forever in production
    This is fine for my own projects since static resources are usually at most 10-20MB of data. Not a lot of memory to sacrifice for the performance gained. But server-side caching should be configurable.
  2. Cache headers (for client side caching) are hardcoded
    Only applies in production, no client-side caching in development mode. Client-side caching should ideally be configurable all the way down to individual resource level.
  3. No support for localized resources
    Currently planning on adding a languages parameter to resource URLs to handle those.
  4. Needs a way to force clients to reload cached resources
    The old ERXResourceManager had a clever trick, allowing the adding of a version query parameter to resource URLs, forcing clients to update potentially cached resources on the client side. This is useful so I'd like to add something like that.
  5. I'm sure there are other edge cases to address. But after some time using this method in a dozen applications, I haven't hit them. Pointers appreciated if you can think of any.

I've deleted most of the code in wonder-slim for supporting traditional split installs, most or all of it for generating URLs during development. It's a lot of logic to maintain and slim is all about reducing complexity and development effort by having a "single, proper, well understood and maintained way" to do stuff. And I think serving resources through the application is most definitely the single proper way.

Static resources are just as much a part of the application as anything dynamic.

What's happenin'

🌶 cayenne CAY-2905 Upgrade Gradle to 8.14 Nov 5
🤸‍♀️ wonder-slim Add some common image mimetypes Nov 4
⚙️️ wonder-slim-deployment Remove unused import Nov 4
🤸‍♀️ wonder-slim Added ERXErrorPage Nov 3
🤸‍♀️ wonder-slim Comments cleanup in app-based WebServerResource management Nov 3
🤸‍♀️ wonder-slim Notification parameter unused, replace with _ Nov 3
🤸‍♀️ wonder-slim Initialize ERXShutdownHook from within Nov 3
🤸‍♀️ wonder-slim Made XXLifecycleListenerHack sound a little more innocent Nov 3
🤸‍♀️ wonder-slim Move ERXApp._cachedApplicationName to top with other fields Nov 2
🤸‍♀️ wonder-slim Start cleanup in ERXApp.name() Nov 2