Skip to content

perf: Cache public lookups, fingerprint assets, cut layout shift - #575

Closed
linkdotnet wants to merge 3 commits into
masterfrom
perf/lookups-render-layout-shift
Closed

linkdotnet wants to merge 3 commits into
masterfrom
perf/lookups-render-layout-shift

Conversation

@linkdotnet

@linkdotnet linkdotnet commented Nov 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Follow-up to f0312d8: removes repeated lookups, revalidation requests and avoidable layout shift on public pages. No public component contract changes.

 ShowBlogPostPage
-  ShortCodeRepository.GetAllAsync()             # every visit
+  ShortCodeCache.GetAsync()                      # cleared by ShortCodesPage store/delete
   SimilarBlogPostSection.OnParametersSetAsync
-    2 queries × every parent render (~6)
+    skip if same post; FusionCache "similar/{id}", tagged
 SimilarBlogPostJob (runs on publish, save, delete)
+  RemoveByTagAsync(CacheTag) after rebuild
   OnAfterRender(first)
-    StoreUserRecordAsync → IsBookmarked
+    IsBookmarked → StoreUserRecordAsync          # icon no longer waits on a DB write
 ArchivePage
-  project p.Slug                                 # unmapped → full entity incl. Content
+  project Title, Slug computed on the record
 BookmarkService
-  ContainsKey + Get (+ Set) per card
+  concurrent callers share one pending read; later pages read fresh
 App.razor
-  <link href="css/basic.css">                    # no-cache, revalidated every load
+  <link href="@Assets["css/basic.css"]">         # fingerprinted, immutable
+  <ImportMap />                                  # .razor.js imports hit hashed files
 ThemeToggler
-  render one icon after 3 encrypted-storage interop calls
+  render both, CSS on :root[data-bs-theme] picks one; click → JS toggleTheme()
 ReadingIndicator.razor.js
-  addEventListener('scroll') per post visit
+  one passive, rAF-throttled listener; tracks the current post's elements

Also: home cards re-render only when bookmarked, Like reads storage once per post, static JsonSerializerOptions in StructuredData, code blocks get the highlight.js box up front, post images use height:auto with decoding="async" and fetchpriority="high" on the first, cards use contain-intrinsic-size: auto 300px.

Evidence

Headless Chrome against dummy data, baseline from a worktree at master:

Before After
Post visit, first (DB commands) 8 4
Post visit, repeat (DB commands) 9 1 (visit record insert)
Archive SQL selects Content yes no
Scroll listeners after post → home → post 2 1
Theme icon before Blazor starts, light scheme moon sun
Local CSS/JS URLs plain, no-cache hashed, max-age=31536000, immutable¹

¹ Development serves even hashed URLs as no-cache; the header is from the endpoint manifest.

Tests: unit 446 passed, integration 229 passed. New:

BookmarkService: 3 concurrent IsBookmarked → 1 storage read
BookmarkService: a later check sees a bookmark another tab added
SimilarBlogPostJob: rebuild evicts cached similar-post lists
SimilarBlogPostSection: re-render with the same post → 1 query
ThemeToggler: both icons rendered, no interop until click; click → toggleTheme

Merge Danger

Door: two-way

Blast Radius: public pages

  • Visitors who only have the old encrypted preferred-theme key fall back to the system preference once.
  • Similar posts and shortcodes are now cached. SimilarBlogPostJob evicts similar-post lists after each rebuild; the shortcodes page evicts shortcodes. Shortcodes written to the DB any other way stay cached up to 7 days.
  • Images without declared dimensions still shift; code-block and image CLS gains weren't measurable locally (CDN theme loads too fast).

Copilot AI balanced review requested due to automatic review settings November 9, 2026 21:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved cache invalidation gaps leave bookmarks stale across tabs and deleted articles visible in recommendations with broken links.

2 open issues
What changed in this PR

This PR improves public-page performance by caching repeated lookups, fingerprinting local assets, and reducing unnecessary rendering and layout shifts.

Changes:

  • Cache shortcodes, similar posts, and bookmark reads; streamline archive projections.
  • Fingerprint assets and simplify theme switching and reading-progress updates.
  • Adjust image and code-block styling, with tests for the updated behavior.
File Description
tests/​LinkDotNet.Blog.UnitTests/​Web/​Features/​ShowBlogPost/​ShowBlogPostPageTests.cs Register caching for post-page tests.
tests/​LinkDotNet.Blog.UnitTests/​Web/​Features/​MarkdownConverterTests.cs Update expected image loading hints.
tests/​LinkDotNet.Blog.UnitTests/​Web/​Features/​Home/​Components/​ThemeTogglerTests.cs Verify both icons and click-only interop.
tests/​LinkDotNet.Blog.UnitTests/​Web/​Features/​Bookmarks/​BookmarkServiceTests.cs Cover shared reads and cross-tab write preservation.
tests/​LinkDotNet.Blog.IntegrationTests/​Web/​Features/​ShowBlogPost/​ShowBlogPostPageTests.cs Register caching in integration fixtures.
tests/​LinkDotNet.Blog.IntegrationTests/​Web/​Features/​ShowBlogPost/​Components/​SimilarBlogPostSectionTests.cs Check repeated renders avoid duplicate queries.
tests/​LinkDotNet.Blog.IntegrationTests/​Web/​Features/​Admin/​ShortCodes/​ShortCodesPageTests.cs Support shortcode cache invalidation in tests.
tests/​LinkDotNet.Blog.IntegrationTests/​BlogPostMarkdownTests.cs Clear shortcode cache after seeding.
src/​LinkDotNet.Blog.Web/​wwwroot/​css/​basic.css Stabilize code blocks, images, cards, and theme icons.
src/​LinkDotNet.Blog.Web/​Features/​ShowBlogPost/​ShowBlogPostPage.razor Cache shortcodes and load bookmark state before recording visits.
src/​LinkDotNet.Blog.Web/​Features/​ShowBlogPost/​Components/​StructuredData.razor Reuse serializer options.
src/​LinkDotNet.Blog.Web/​Features/​ShowBlogPost/​Components/​SimilarBlogPostSection.razor Cache recommendations and skip same-post reloads.
src/​LinkDotNet.Blog.Web/​Features/​ShowBlogPost/​Components/​ReadingIndicator.razor.js Reuse one passive, frame-throttled scroll listener.
src/​LinkDotNet.Blog.Web/​Features/​ShowBlogPost/​Components/​Like.razor Avoid repeated storage reads for the same post.
src/​LinkDotNet.Blog.Web/​Features/​ShowBlogPost/​BlogPostMarkdownMiddleware.cs Use cached shortcodes for Markdown responses.
src/​LinkDotNet.Blog.Web/​Features/​Services/​ShortCodeCache.cs Add shared shortcode caching and eviction.
src/​LinkDotNet.Blog.Web/​Features/​LazyLoadImageExtension.cs Add image decoding and priority hints.
src/​LinkDotNet.Blog.Web/​Features/​Home/​Components/​ThemeToggler.razor.js Toggle and persist the theme directly.
src/​LinkDotNet.Blog.Web/​Features/​Home/​Components/​ThemeToggler.razor Render both icons and defer interop until clicking.
src/​LinkDotNet.Blog.Web/​Features/​Components/​ShortBlogPost.razor Skip unnecessary bookmark-state renders.
src/​LinkDotNet.Blog.Web/​Features/​Bookmarks/​BookmarkService.cs Share bookmark reads and refresh storage before writes.
src/​LinkDotNet.Blog.Web/​Features/​Archive/​ArchivePage.razor Compute slugs outside the database projection.
src/​LinkDotNet.Blog.Web/​Features/​Admin/​ShortCodes/​ShortCodesPage.razor Evict cached shortcodes after saves and deletions.
src/​LinkDotNet.Blog.Web/​App.razor Use fingerprinted asset URLs and an import map.

Review effort: Balanced

Comment on lines +89 to +92
similarBlogPosts = await FusionCache.GetOrSetAsync<IReadOnlyCollection<BlogPost>>(
$"similar/{BlogPost.Id}",
async _ => await LoadSimilarBlogPostsAsync(BlogPost.Id),
options => options.SetDuration(TimeSpan.FromMinutes(AppConfiguration.Value.FirstPageCacheDurationInMinutes)));

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in db79b25: entries are tagged SimilarBlogPostJob.CacheTag and the job evicts the tag after each rebuild. Delete, publish and save all run the job.

Copilot AI balanced review requested due to automatic review settings November 9, 2026 21:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Restoring an article version neither invalidates nor rebuilds the related-post cache, leaving fresh visits with stale titles, descriptions, and slugs until the cache expires.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

Medium severity Restore fails to invalidate cached related-post data

src/​LinkDotNet.Blog.Web/​Features/​ShowBlogPost/​Components/​SimilarBlogPostSection.razor:90

After a related-post list is cached, restoring an article version leaves fresh visits showing its pre-restore title, description, and slug until the cache expires. The restore path saves the article but neither invalidates SimilarBlogPostJob.CacheTag nor runs the rebuild used by normal saves. Invalidate that tag or trigger the rebuild after restoration, and add a test that warms the cache before restoring a version.

1 resolved since last review

Review effort: Balanced

@linkdotnet

Copy link
Copy Markdown
Owner Author

Restore path fixed in d2826b4: restoring a version now runs SimilarBlogPostJob like a normal save, which recomputes similarities and evicts SimilarBlogPostJob.CacheTag. Covered by GivenVersionRestore_SimilarBlogPostsAreRecalculated; eviction itself by ShouldEvictCachedSimilarBlogPostsAfterRecalculating.

Copilot AI balanced review requested due to automatic review settings November 9, 2026 21:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues remain for the public-page performance changes.

1 open issue

Review effort: Balanced

@linkdotnet linkdotnet closed this Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants