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.
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.
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
Even if already used in production, wonder-slim's implementation still needs some work:
languages parameter to resource URLs to handle those.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.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.
| 🚀 ng-objects | NGPopUpButton: fail deliberately on out-of-range selection index | Aug 11 |
| 🚀 ng-objects | NGPopUpButton: selection binding is the sole authority for rendered s… | Aug 11 |
| 🤸♀️ wonder-slim | Log pid | Aug 11 |
| 🚀 ng-objects | Log pid | Aug 11 |
| 🚀 ng-objects | /ng/dev/eval: reject a form-encoded body with a clear error, don't guess | Aug 10 |
| 🚀 ng-objects | NGRequest: FIXME on the remoteAddress accessors | Aug 10 |
| 🤸♀️ wonder-slim | Move dev-loop machinery to er.extensions.dev | Aug 10 |
| 💋 parslips | Dev server: element-API lookup endpoint + a plain-tag binding lint | Aug 10 |
| 🤸♀️ wonder-slim | Add eval and problems dev request handlers (aligned with ng-objects) | Aug 10 |
| 🚀 ng-objects | Add /ng/dev/eval and /ng/dev/problems development endpoints | Aug 10 |
| 🤸♀️ wonder-slim | 8.0.3 | Jun 26 |
| 🌿 parsley | 1.6.0 | Jun 26 |
| 🌿 parsley | 1.5.0 | Jun 18 |
| 🤸♀️ wonder-slim | 8.0.2 | Jun 18 |
| 🦡 vermilingua | 1.1.5 | Jun 15 |
| 🤸♀️ wonder-slim | 8.0.1 | Jun 1 |
| 🌿 parsley | 1.4.2 | Jun 1 |
| 🦡 vermilingua | 1.1.4 | Apr 24 |
| 🚀 ng-objects | 0.1.1 | Apr 22 |
| 🦡 vermilingua | 1.1.3 | Apr 15 |
| 💋 parslips | Dev server surface: planned endpoints + conventions (catch-all roadmap) | Aug 10 |
| 🚀 ng-objects | NGRequest: parse the body lazily (raw body + on-demand form values), not eagerly at the adaptor | Aug 10 |
| 🚀 ng-objects | Headless action driving: render a page, list its actions, invoke one (dev endpoint) | Aug 10 |
| 🤸♀️ wonder-slim | Make ERXWOImage the full <wo:img> replacement (incl. dynamic/data images) | Jul 8 |
| 🌿 parsley | Heat map: make render-tree sibling ordering user-selectable (hottest-first vs source order) | Jul 6 |
| 🚀 ng-objects | Parser: record a SourceRange per binding (not just per element) | Jul 6 |
| 🧩 apiext-format | Generated-from-source authoring mode (annotations + javadoc → .apiext); the file stays canonical | Jul 5 |
| 🤸♀️ wonder-slim | AjaxSlim: remove replaceID (the ajax-replacement mode) | Jul 5 |
| 🧩 apiext-format | Prefix-family bindings (declare a family of bindings by prefix, e.g. ?key query params) | Jul 4 |
| 🧩 apiext-format | Enclosing-context requirement (element must sit inside a form) | Jul 4 |