Skip to main content

max / makenotwork

Performance pass: layout shift fixes, reload elimination, style guide Philosophy: - Add performance_philosophy.md (Tufte/McMaster-Carr principles for MNW) - Add frontend performance section to CONTRIBUTING.md Layout shift: - Add aspect-ratio to images on item page, audio player, wizards - Buy page already correct (has aspect-ratio in CSS class) Reload elimination: - Item settings: 4x window.location.reload → tab re-click - Item details: primary tag set → tab re-click - User profile: domain removal → tab re-click Loading states: - Remove "Loading..." placeholder text from 2FA and passkey sections Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-05 18:34 UTC
Commit: 2006499113fc244c3776261880ef1acca164c15b
Parent: 46bdd28
11 files changed, +138 insertions, -18 deletions
@@ -278,6 +278,59 @@ Every full-page template needs at minimum:
278 278 - `csrf_token: Option<String>` — for the CSRF meta tag
279 279 - `session_user: Option<SessionUser>` — for the header (login state, avatar)
280 280
281 + ## Frontend Performance
282 +
283 + These rules apply to all HTML templates. The goal is zero layout shift, no wasted pixels, and instant-feeling interactions.
284 +
285 + ### Images
286 +
287 + Every `<img>` must have explicit dimensions to prevent layout shift:
288 + ```html
289 + <!-- Inline style with both width and height -->
290 + <img src="{{ url }}" alt="{{ title }}"
291 + style="width: 120px; height: 120px; object-fit: cover;">
292 +
293 + <!-- Or use aspect-ratio for responsive images -->
294 + <img src="{{ url }}" alt="{{ title }}"
295 + style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
296 + ```
297 +
298 + Never use `loading="lazy"` for images that may appear above the fold (the first visible screen before scrolling).
299 +
300 + ### Navigation and Reloads
301 +
302 + Never use `window.location.reload()` when an HTMX partial swap can do the job. Full-page reloads discard all client state, re-parse all JS/CSS, and feel slow.
303 +
304 + ```html
305 + <!-- Bad: full-page reload after action -->
306 + <button hx-put="/api/items/{{ id }}"
307 + hx-on::after-request="if(event.detail.successful) window.location.reload()">
308 +
309 + <!-- Good: re-fetch the relevant tab/section -->
310 + <button hx-put="/api/items/{{ id }}"
311 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">
312 + ```
313 +
314 + Legitimate uses of `window.location.href`:
315 + - Navigating to a different page entirely (login, checkout redirect, file download)
316 + - After destructive account actions (delete account → login page)
317 +
318 + ### Loading States
319 +
320 + Prefer server-complete responses over client-side loading placeholders. The server should wait for data and send a complete fragment in one paint, rather than sending a skeleton and filling it in later.
321 +
322 + Use `hx-trigger="revealed"` with a loading placeholder only when the data is expensive to fetch AND hidden behind a `<details>` element (e.g., 2FA status, passkey list). Never use loading placeholders for content that's visible on initial tab load.
323 +
324 + ### Inline Scripts
325 +
326 + Keep inline `<script>` blocks under 20 lines. Extract larger scripts to static JS files in `server/static/`. Static files are cached by the browser; inline scripts are re-parsed on every HTMX fragment swap.
327 +
328 + ### Information Density
329 +
330 + Show more data in less space. Prefer tight rows over hero cards. Let users compare by scanning, not by clicking into detail pages. A page that shows 15 items is more useful than one that shows 3.
331 +
332 + Use typography (weight, size) and whitespace for hierarchy instead of borders and background colors. A 16px gap groups elements as well as a 1px border, with less visual noise.
333 +
281 334 ## CSRF Protection
282 335
283 336 The CSRF middleware (`src/csrf.rs`) validates all state-changing requests (POST, PUT, PATCH, DELETE) except exempted paths (webhooks, auth endpoints, OAuth).
@@ -0,0 +1,64 @@
1 + # Performance Philosophy
2 +
3 + Tufte's core insight: respect the user by showing them the data. McMaster-Carr proved it at scale — 700,000 products, no decoration, universally praised. The best interface disappears, leaving only the information the user came for.
4 +
5 + ## Principles
6 +
7 + ### 1. Every Element Earns Its Place
8 +
9 + Tufte's data-ink ratio: of all pixels on a page, what fraction communicates actual information? Maximize that fraction. McMaster-Carr has no hero images, no banners, no carousels. Their search results are dense text because text is faster to scan than thumbnails.
10 +
11 + A storefront shows the creator's work, not the platform's branding. Navigation is tight. Metadata (price, format, date, size) is visible without clicking. If a UI element doesn't help the user buy, sell, or browse, it doesn't ship.
12 +
13 + ### 2. Speed Is the Design
14 +
15 + McMaster-Carr's search feels instant because it is instant. Slow interfaces are a form of chartjunk — decoration where information should be.
16 +
17 + Server-rendered HTML + HTMX is the right architecture. A 50ms server response with a complete HTML fragment always feels faster than a 200ms JSON payload that a client framework then renders. Commitments:
18 +
19 + - No full-page reloads for navigation. HTMX swaps fragments.
20 + - No layout shift. Elements have known dimensions before content arrives.
21 + - No lazy-loading above the fold. First screen is complete on first paint.
22 + - Measure time-to-interactive, not time-to-first-byte. The user's clock starts when they click.
23 + - Pre-fetch on hover when the next action is predictable (tabs, navigation links).
24 +
25 + ### 3. Density Is Generosity
26 +
27 + Dense information is easier to read, not harder, when the design is clean. The problem was never complexity — it was chartjunk masquerading as organization. Minard fit six variables onto one map. McMaster-Carr puts dozens of products on a screen without scrolling.
28 +
29 + A catalog page should show 15-20 items without scrolling, not 3 hero cards. Each item gets a tight row: title, price, format badge, one-line description. Users compare by scanning, not by clicking into detail pages and pressing back.
30 +
31 + ### 4. Hierarchy Through Typography, Not Decoration
32 +
33 + McMaster-Carr uses almost no color. Hierarchy comes from weight, size, and spacing. Tufte's "smallest effective difference" — the least visual distinction that still communicates structure.
34 +
35 + - Weight and size for titles vs metadata vs body.
36 + - Whitespace to group related elements.
37 + - Consistent alignment so the eye can scan columns.
38 + - Borders only when spacing alone is ambiguous.
39 +
40 + ### 5. Complexity When the Data Demands It
41 +
42 + Tufte's sharpest criticism is oversimplification — the PowerPoint mentality. McMaster-Carr never hides specs behind "show more." If a bolt has 14 dimensions, all 14 are visible.
43 +
44 + A creator selling a sample pack might have format, sample rate, BPM, key, instrument, duration, license, and tier. Show all of it. Don't collapse it to make the page look cleaner. Clean is not the goal. Clear is the goal.
45 +
46 + ### 6. Refuse to Waste Attention
47 +
48 + McMaster-Carr has no concept of "engagement." No notification badges, no algorithmic feeds, no interruptions during checkout. The user has a task. The site completes it.
49 +
50 + - No interstitials.
51 + - No dark patterns. "No thanks" is the same size as "yes."
52 + - Search that works. Server-side, fast, honest results.
53 + - Checkout in as few steps as possible.
54 +
55 + ## Applied to MNW
56 +
57 + MNW already has the right foundations: server-rendered HTML, HTMX, warm beige/charcoal palette, intentional three-tier typography (Young Serif / IBM Plex Mono / Lato), no SPA overhead. The performance pass is about removing what slows things down, not adding what speeds things up.
58 +
59 + Concrete targets:
60 + - Dashboard tab switch: < 100ms perceived (pre-fetch on hover)
61 + - Discover page filter: < 150ms (HTMX partial swap, no full reload)
62 + - Item purchase to download: < 3 clicks from any page
63 + - Public project page: complete first paint with no layout shift
64 + - Zero skeleton loaders — if data isn't ready, the server waits; the browser paints once
@@ -30,7 +30,12 @@ Split checkout.rs (792 -> 434 + 308 helpers). yara-x bumped 1.14->1.15 (intaglio
30 30 Dashboard restructure complete (Phases 1-6 in todo_done.md). Tab layout: Projects, Payments, Analytics, Profile, Account, Plan + overflow. Fan/creator progressive disclosure implemented. Labels removed. Jargon renamed.
31 31
32 32 #### Performance
33 - - [ ] Cross-site performance pass — pre-fetch tab data on hover, optimize perceived load times
33 + - [x] Add performance philosophy doc (`docs/performance_philosophy.md`) — Tufte/McMaster-Carr principles applied to MNW
34 + - [x] Add frontend performance rules to CONTRIBUTING.md — image dimensions, reload avoidance, loading states, density
35 + - [x] Fix layout shift: add aspect-ratio to all images missing dimensions (item page, audio player, buy page, wizards)
36 + - [x] Replace `window.location.reload()` with HTMX tab re-fetch on item settings (4x), item details (1x), user profile (2x)
37 + - [x] Remove "Loading..." placeholder text from 2FA and passkey sections (empty until revealed)
38 + - [ ] Extract heavy inline scripts to static JS — item_details.html (278 lines), upload handlers (~250 lines combined). Requires data-attribute refactor for template variables
34 39
35 40 #### Discoverability
36 41 - [ ] Add Media Library access from content editors — "Insert Image" button in blog/item editors
@@ -459,7 +459,7 @@
459 459 {% match item.content %}
460 460 {% when crate::types::ItemContent::Audio with { cover_url, .. } %}
461 461 {% if let Some(url) = cover_url %}
462 - <img src="{{ url }}" alt="{{ item.title }} cover art">
462 + <img src="{{ url }}" alt="{{ item.title }} cover art" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
463 463 {% else %}
464 464 [Cover Art]
465 465 {% endif %}
@@ -155,7 +155,7 @@
155 155 {% if item.cover_image_url.is_some() %}
156 156 <div class="item-media">
157 157 {% if let Some(img) = item.cover_image_url %}
158 - <img src="{{ img }}" alt="{{ item.title }}" style="width: 100%; display: block;">
158 + <img src="{{ img }}" alt="{{ item.title }}" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block;">
159 159 {% endif %}
160 160 </div>
161 161 {% endif %}
@@ -66,7 +66,7 @@
66 66 hx-put="/api/items/{{ item.id }}/primary-tag"
67 67 hx-vals='{"tag_id": "{{ tag.id }}"}'
68 68 hx-swap="none"
69 - hx-on::after-request="window.location.reload()">&#9734;</button>
69 + hx-on::after-request="document.getElementById('tab-details').click()">&#9734;</button>
70 70 {% endif %}
71 71 </span>
72 72 {% endfor %}
@@ -13,7 +13,7 @@
13 13 hx-put="/api/items/{{ item.id }}"
14 14 hx-vals='{"publish_at": ""}'
15 15 hx-confirm="Cancel the scheduled publish?"
16 - hx-on::after-request="if(event.detail.successful) window.location.reload()">Cancel Schedule</button>
16 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">Cancel Schedule</button>
17 17 </div>
18 18 {% else if item.is_public %}
19 19 <!-- Published: show unpublish button -->
@@ -25,7 +25,7 @@
25 25 hx-put="/api/items/{{ item.id }}"
26 26 hx-vals='{"is_public": "false"}'
27 27 hx-confirm="Unpublish this item? It will be hidden from public view."
28 - hx-on::after-request="if(event.detail.successful) window.location.reload()">Unpublish Item</button>
28 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">Unpublish Item</button>
29 29 </div>
30 30 {% else %}
31 31 <!-- Draft: show publish now + schedule -->
@@ -37,12 +37,12 @@
37 37 hx-put="/api/items/{{ item.id }}"
38 38 hx-vals='{"is_public": "true"}'
39 39 hx-confirm="Publish this item?"
40 - hx-on::after-request="if(event.detail.successful) window.location.reload()">Publish Now</button>
40 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">Publish Now</button>
41 41 <button class="secondary" onclick="document.getElementById('schedule-form').style.display='block'">Schedule</button>
42 42 </div>
43 43 <form id="schedule-form" style="display: none; margin-top: 1rem;"
44 44 hx-put="/api/items/{{ item.id }}"
45 - hx-on::after-request="if(event.detail.successful) window.location.reload()">
45 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-settings').click()">
46 46 <div class="form-group">
47 47 <label for="publish-at">Publish at (UTC)</label>
48 48 <input type="datetime-local" id="publish-at" name="publish_at" required>
@@ -93,7 +93,6 @@
93 93 hx-get="/api/users/me/totp/status"
94 94 hx-trigger="revealed"
95 95 hx-swap="innerHTML">
96 - <p style="opacity: 0.7;">Loading...</p>
97 96 </div>
98 97 </details>
99 98
@@ -103,7 +102,6 @@
103 102 hx-get="/api/users/me/passkeys"
104 103 hx-trigger="revealed"
105 104 hx-swap="innerHTML">
106 - <p style="opacity: 0.7;">Loading...</p>
107 105 </div>
108 106 </details>
109 107
@@ -171,7 +171,7 @@
171 171 hx-confirm="Remove this domain?"
172 172 hx-target="closest .form-section"
173 173 hx-swap="outerHTML"
174 - hx-on::after-request="if(event.detail.successful) location.reload()">Remove</button>
174 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-profile').click()">Remove</button>
175 175 </div>
176 176 <div id="domain-verify-result" style="margin-top: 0.75rem;"></div>
177 177 {% else %}
@@ -183,7 +183,7 @@
183 183 hx-confirm="Remove this domain? Your domain will stop serving your content."
184 184 hx-target="closest .form-section"
185 185 hx-swap="outerHTML"
186 - hx-on::after-request="if(event.detail.successful) location.reload()">Remove Domain</button>
186 + hx-on::after-request="if(event.detail.successful) document.getElementById('tab-profile').click()">Remove Domain</button>
187 187 {% endif %}
188 188 {% when None %}
189 189 <form hx-post="/api/domains"
@@ -27,7 +27,7 @@
27 27 <div class="project-image-dropzone" id="image-dropzone">
28 28 {% if let Some(url) = cover_image_url %}
29 29 <div class="project-image-current" id="image-current">
30 - <img src="{{ url }}" alt="Item image">
30 + <img src="{{ url }}" alt="Item image" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
31 31 </div>
32 32 {% else %}
33 33 <div class="project-image-placeholder" id="image-placeholder">
@@ -154,7 +154,7 @@
154 154 var div = document.createElement('div');
155 155 div.className = 'project-image-current';
156 156 div.id = 'image-current';
157 - div.innerHTML = '<img src="' + url + '" alt="Item image">';
157 + div.innerHTML = '<img src="' + url + '" alt="Item image" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">';
158 158 dropzone.insertBefore(div, fileInput);
159 159 currentImg = div;
160 160 }
@@ -15,7 +15,7 @@
15 15 <div class="project-image-dropzone" id="image-dropzone">
16 16 {% if let Some(url) = cover_image_url %}
17 17 <div class="project-image-current" id="image-current">
18 - <img src="{{ url }}" alt="Project image">
18 + <img src="{{ url }}" alt="Project image" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
19 19 </div>
20 20 {% else %}
21 21 <div class="project-image-placeholder" id="image-placeholder">
@@ -52,7 +52,7 @@
52 52 <div class="project-card-preview">
53 53 <div class="preview-cover" id="preview-cover">
54 54 {% if let Some(url) = cover_image_url %}
55 - <img src="{{ url }}" alt="Preview">
55 + <img src="{{ url }}" alt="Preview" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">
56 56 {% else %}
57 57 <div class="preview-cover-empty">No image</div>
58 58 {% endif %}
@@ -162,12 +162,12 @@
162 162 var div = document.createElement('div');
163 163 div.className = 'project-image-current';
164 164 div.id = 'image-current';
165 - div.innerHTML = '<img src="' + url + '" alt="Project image">';
165 + div.innerHTML = '<img src="' + url + '" alt="Project image" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">';
166 166 dropzone.insertBefore(div, fileInput);
167 167 currentImg = div;
168 168 }
169 169 var preview = document.getElementById('preview-cover');
170 - preview.innerHTML = '<img src="' + url + '" alt="Preview">';
170 + preview.innerHTML = '<img src="' + url + '" alt="Preview" style="width: 100%; aspect-ratio: 1 / 1; object-fit: cover;">';
171 171 }
172 172
173 173 function showError(message) {