Western Dental
I was brought in to make the site fast. The HTML alone was taking around a second and a half to arrive, which is a server problem rather than a frontend one.
Western Dental runs over 190 dental and orthodontics offices several states.
- Why It Was Slow. Every office had its own page, nested by state and then city, and many of those offices had a page per service beneath them. The whole thing was mirrored in Spanish under a separate site root. That adds up to thousands of content nodes, and Umbraco is not especially quick at working with large numbers of them. Code that walked the content tree on every request was very slow.
- The Cheap Wins First. Before touching the server side there was ordinary delivery work: reordering script and style tags so they stopped blocking rendering, and putting real caching headers on static files, which were going out with expiry dates already in the past. The homepage pulled down about a megabyte of CSS and JavaScript, an unminified copy of jQuery was loading twice, and the same bundle was served from a separate URL per language, so switching between English and Spanish downloaded all of it again.
- Measure First. Time to first byte tells you how long the server spent building the page, and once you subtract network latency, more than roughly 200 milliseconds of server-side processing is worth investigating, and this site was well past that. To narrow it down to specific files, I commented out each view and layout in turn and compared the page load time against the baseline, which gave me an ordered list of what to fix. A stopwatch wrapping individual partial view renders found the rest.
- Stop Walking the Tree. The worst offender was
Descendants(), which crawls a large part of the content tree on every request. I wrote a cached version that stores the resulting node IDs rather than the content objects, since those objects can carry per-request state and are not safe to hold between requests. The tree gets walked once and later requests just resolve IDs. - Skip the Lookup Entirely. A lot of traversal does not need to happen at all. Instead of crawling the tree for a section root, put a content picker on the homepage and read the node straight off it. Instead of crawling up the ancestor chain to work out which homepage a given page belongs to, read the
Pathproperty, which already holds the ancestor IDs as a comma-delimited list. - Cheaper Objects, Fewer of Them. Switched dynamically typed content access to the strongly typed equivalent, which is faster for the same work. Stopped constructing a new
UmbracoHelperthroughout the codebase and reused one per request, held inHttpContext.Items, since it can change between requests but not within one. - Application-Level Caching, Not Output Caching. Work that is expensive no matter how you approach it gets mapped onto small purpose-built objects and cached in memory, with invalidation when an editor publishes. I stayed away from output caching, which only pays off after a page has already been visited, so on a site with thousands of pages most visitors would still wait the full amount. Caching at the application level means one visitor absorbs the cost and every page benefits afterward.
- Backporting the Tools. The caching came from a pair of small libraries I had open sourced, which targeted a newer version of Umbraco than this site ran.












