Architecture Review

SSR in .NET MVC vs. Lit Web Components

Why the current Vite + Lit + Tailwind setup is fine as a build workflow, but risky for the project's SEO goal — with the actual request/response flows laid out below.

Current setup

  • .NET 10 MVC, SSR by design, for a site where SEO is a hard requirement
  • Hard constraint: no Node.js anywhere in the pipeline, including CI/CD
  • Vite + Lit used locally to build Web Components
  • Tailwind CSS v4 for styling
  • Bundle is built on a dev machine, then copied into wwwroot and committed

The short version

Keeping Node out of CI/CD is solvable — Tailwind v4 even ships a Node-free standalone CLI binary. The part that actually threatens SEO is architectural: Lit renders into Shadow DOM, and that only happens after the browser downloads and runs JavaScript. Anything living in that gap is invisible to crawlers that don't execute JS, and delayed for the ones that do.

sequenceDiagram autonumber participant U as Browser / Crawler participant S as ASP.NET Core Server participant C as MVC Controller participant D as Database participant V as Razor View Engine U->>S: GET /product/123 S->>C: Route matched C->>D: Query product data D-->>C: Return data C->>V: Render with model V-->>C: Complete HTML (text, links, meta) C-->>S: ViewResult S-->>U: 200 OK — full HTML Note over U: Content exists the instant
the response arrives. No JS required.

Classic SSR: everything a crawler needs is already in the first response.

sequenceDiagram autonumber participant U as Browser / Crawler participant S as ASP.NET Core Server participant C as MVC Controller participant V as Razor View Engine participant B as bundle.js U->>S: GET /product/123 S->>C: Route matched C->>V: Render page shell V-->>C: HTML with empty <product-widget>
+ <script src=bundle.js> C-->>S: ViewResult S-->>U: 200 OK — content not in the HTML yet Note over U: A non-JS crawler stops here. U->>S: GET bundle.js S-->>U: Lit + Tailwind bundle U->>U: Execute JS, upgrade element U->>U: Shadow DOM renders — content appears Note over U: Extra round trip + JS execution
before the content exists.

Same route, but content depends on a second round trip and JS execution succeeding.

Time until content actually exists

Razor SSR waiting…
Lit Web Component (Shadow DOM) waiting…
Press run to simulate both requests on the same timeline (times are illustrative, not measured).

Risks

high

Content hidden behind JS execution

Page copy, headings or links placed inside Shadow DOM are invisible to crawlers that don't render JS, and delayed for ones that do.

medium

FOUC / layout shift

Content popping in after hydration hurts perceived performance and Core Web Vitals (CLS).

medium

Tailwind vs. Shadow DOM

Global utility classes don't cross the shadow boundary — the compiled stylesheet must be injected per component via adoptedStyleSheets, duplicating CSS.

medium

Build/source drift

Nothing verifies a committed bundle still matches its source, since CI can't rebuild it to check.

Recommendations

0 / 6 addressed