Final Screen Size Solution

Static generation + Progressive enhancement + No hydration mismatch

Solution Comparison

Server-side headers() vs Our hybrid approach

FeatureServer-side headers()
(Initial Problem)
Our Hybrid Solution
(Current Implementation)
Static Generation (SSG/ISR)
Broken
headers() makes entire app dynamic
Works
No headers() usage = truly static
Build Time Generation
None
No static HTML generated
Static HTML
Generated with sensible defaults
Performance
Poor
Dynamic rendering on every request
Excellent
Static serving + progressive enhancement
CDN Caching
Disabled
Dynamic pages can't be cached
Enabled
Static pages cached globally
Server Components Support
Yes
Can use headers() directly
Yes
Can use static hints via Promise
Hydration Mismatch
None
Server and client have same data
None
Progressive enhancement prevents mismatch
Screen Size Accuracy
Limited
Only device type hints from User-Agent
Exact
Device hints + exact pixel measurements
Resize Updates
No
Static after initial render
Real-time
Updates on window resize events
Development Experience
Complex
Must understand dynamic rendering implications
Simple
Works like any other React context
Overall Recommendation
Avoid
Breaks static optimization
Recommended
Best of both worlds

❌ Server-side headers() Problems:

  • • Breaks static generation completely
  • • Poor performance (dynamic rendering)
  • • No CDN caching benefits
  • • Limited screen size information

✅ Our Hybrid Solution Benefits:

  • • Maintains static generation
  • • Excellent performance
  • • Full CDN caching support
  • • Exact screen measurements + device hints

📋 Implementation Walkthrough

Here's how our hybrid solution works step-by-step, with actual code snippets from the implementation:

Step 1: Static Hint Generation

No headers() usage = Keeps everything static

// lib/screen-size-hint.ts
export interface ScreenSizeHint {
  buildTimeDefaults: {
    isMobile: boolean
    isTablet: boolean
    isDesktop: boolean
    deviceType: "desktop"
  }
  timestamp: number
}

export async function getStaticScreenSizeHint(): Promise<ScreenSizeHint> {
  // ✅ NO headers() usage - this keeps it static!
  return {
    buildTimeDefaults: {
      isMobile: false,
      isTablet: false,
      isDesktop: true,
      deviceType: "desktop", // Safe default for build time
    },
    timestamp: Date.now(),
  }
}

Key: This function returns sensible defaults without using any dynamic APIs like headers(). This ensures the entire app remains statically generated.

Step 2: Provider with React's use() Hook

Progressive enhancement from static to dynamic

// components/screen-size-provider.tsx
"use client"

import { createContext, useContext, useEffect, useState } from "react"
import { use } from "react" // ✨ React's use() hook
import type { ScreenSizeHint } from "../lib/screen-size-hint"

export function ScreenSizeProvider({
  children,
  staticHintPromise, // ✨ Promise from server
}: {
  children: React.ReactNode
  staticHintPromise: Promise<ScreenSizeHint>
}) {
  // ✨ use() unwraps the Promise from the server
  const staticData = use(staticHintPromise)

  const [isHydrated, setIsHydrated] = useState(false)
  const [serverHint, setServerHint] = useState(null)
  const [clientSize, setClientSize] = useState({
    width: 1024, height: 768,
    isMobile: false, isTablet: false, isDesktop: true,
    breakpoint: "lg" as const,
  })

  useEffect(() => {
    // 🔍 Get device hints from User-Agent (client-side)
    const userAgent = navigator.userAgent
    const isMobile = /Mobile|Android|iPhone/.test(userAgent)
    const isTablet = /iPad|Tablet/.test(userAgent)
    const isDesktop = !isMobile && !isTablet

    setServerHint({ isMobile, isTablet, isDesktop,
      deviceType: isMobile ? "mobile" : isTablet ? "tablet" : "desktop"
    })

    // 📏 Get exact client measurements
    function updateClientSize() {
      const width = window.innerWidth
      const height = window.innerHeight
      // ... calculate breakpoints and device types
      setClientSize({ width, height, isMobile: width < 768, /* ... */ })
    }

    updateClientSize()
    setIsHydrated(true)
    window.addEventListener("resize", updateClientSize)
    return () => window.removeEventListener("resize", updateClientSize)
  }, [])

  return (
    <ScreenSizeContext.Provider value={{
      buildTimeDefaults: staticData.buildTimeDefaults, // ✅ Available immediately
      client: clientSize,     // ✅ Available after hydration
      serverHint,            // ✅ Available after User-Agent detection
      isHydrated
    }}>
      {children}
    </ScreenSizeContext.Provider>
  )
}

Key: The use() hook unwraps the Promise from the server, providing immediate access to static defaults while progressively enhancing with User-Agent hints and exact client measurements.

Step 3: Root Layout Setup

Passing the Promise to the client provider

// app/layout.tsx
import { ScreenSizeProvider } from "../components/screen-size-provider"
import { getStaticScreenSizeHint } from "../lib/screen-size-hint"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  // ✨ Create the Promise on the server (but it's static!)
  const staticHintPromise = getStaticScreenSizeHint()

  return (
    <html lang="en">
      <body>
        {/* ✨ Pass the Promise to the client provider */}
        <ScreenSizeProvider staticHintPromise={staticHintPromise}>
          {children}
        </ScreenSizeProvider>
      </body>
    </html>
  )
}

Key: The server creates a Promise with static defaults and passes it to the client provider. No dynamic APIs are used, so the layout remains statically generated.

Step 4: Using the Hook in Components

Choose the best data source for your needs

// components/adaptive-component.tsx
"use client"

import { useScreenSize } from "./screen-size-provider"

export function AdaptiveComponent() {
  const { buildTimeDefaults, client, serverHint, isHydrated } = useScreenSize()

  // 🎯 Choose the best data source for your use case:
  
  // Option 1: Use server hint when available, fallback to build defaults
  const currentDevice = isHydrated && serverHint ? serverHint : buildTimeDefaults
  
  // Option 2: Wait for exact client measurements
  const exactSize = isHydrated ? client : { width: 1024, isMobile: false }
  
  // Option 3: Progressive enhancement - start with defaults, upgrade to hints
  const deviceType = serverHint?.deviceType || buildTimeDefaults.deviceType

  return (
    <div>
      {currentDevice.isMobile ? (
        <MobileLayout />
      ) : currentDevice.isTablet ? (
        <TabletLayout />
      ) : (
        <DesktopLayout />
      )}
      
      <div>Current width: {exactSize.width}px</div>
      <div>Device type: {deviceType}</div>
    </div>
  )
}

Key: Components can choose between build-time defaults (immediate), server hints (after User-Agent detection), or exact client measurements (after hydration) based on their specific needs.

Step 5: Server Component Usage

Server components can use static hints

// components/server-component.tsx
import { getStaticScreenSizeHint } from "../lib/screen-size-hint"

export async function ServerComponent() {
  // ✅ Server components can use the static hint!
  const hint = await getStaticScreenSizeHint()

  return (
    <div>
      {hint.buildTimeDefaults.isDesktop ? (
        <div>
          <h2>Desktop-optimized content</h2>
          <ComplexDataVisualization />
        </div>
      ) : (
        <div>
          <h2>Mobile-optimized content</h2>
          <SimpleCardLayout />
        </div>
      )}
      
      <p>Generated at: {new Date(hint.timestamp).toLocaleString()}</p>
    </div>
  )
}

Key: Server components can make rendering decisions based on static defaults, then client components can enhance the experience with real device detection.

🔄 How It Works: Timeline

The progressive enhancement process

1

Build Time

Static HTML generated with desktop defaults (isMobile: false, isDesktop: true). // Remember: this is config and it can be tailored to our needs (based on sessions data).

2

Server Response

Static HTML served instantly from CDN with build-time defaults

3

Client Hydration

React hydrates, use() hook unwraps Promise, User-Agent detection begins

4

Device Detection

User-Agent parsed, exact screen measurements taken, components re-render with real data

5

Runtime Updates

Window resize events update measurements in real-time

✅ How This Solution Works

Build Time:

  • Static HTML generated with desktop defaults
  • No headers() usage = truly static
  • ISR/SSG works perfectly
  • Fast CDN serving

Runtime:

  • User-Agent detection for device hints
  • Exact screen measurements
  • Real-time resize updates
  • Progressive enhancement

Server Component with Static Hints

This server component can make rendering decisions based on build-time defaults.

Build-time device type:desktop
Assumes desktop:Yes
Generated at:2:33:50 PM

💡 This content was rendered on the server with static defaults, then enhanced on the client with real device detection.

Adaptive Content

🖥️ Desktop Layout

Optimized for desktop with mouse interactions

Build Time (Static)

Device: desktop

Mobile: No

Desktop: Yes

✅ Statically generated

User-Agent Hint (Loading...)

Detecting...

Client Measurements (Loading...)

Width: 1024px

Height: 768px

Breakpoint: lg

Mobile: No

📏 Exact measurements

🎯 Key Benefits

Performance

  • Static HTML serving
  • CDN cacheable
  • Fast initial load
  • Progressive enhancement

Developer Experience

  • Server components work
  • No hydration mismatch
  • TypeScript support
  • Easy to use hook

Functionality

  • Device type detection
  • Exact screen dimensions
  • Responsive breakpoints
  • Resize event handling