Skip to content

Client Directives Still Include JS in Critical Rendering Path #15606

@VashJuan

Description

@VashJuan

Astro Info

Astro                    v6.0.0-beta.14
Node                     v25.6.1
System                   Windows (x64)
Package Manager          pnpm
Output                   static
Adapter                  none
Integrations             @astrojs/svelte
                         @astrojs/mdx
                         @vite-pwa/astro-integration
                         @astrojs/sitemap

If this issue only occurs in one browser, which browser is a problem?

No response

Describe the Bug

Astro Bug Report: Client Directives Still Include JS in Critical Rendering Path

Description

All Astro client directives (client:load, client:idle, client:only) bundle JavaScript in the initial page load, causing non-critical components to block LCP (Largest Contentful Paint) measurement. There appears to be no way to truly defer a component's JavaScript until after the window load event completes.

Issue

We have a decorative FloatingImages Svelte component that should load after LCP to avoid impacting performance metrics. Despite trying all available client directives, the component's JavaScript always appears in the critical rendering path and delays LCP measurement.

Steps to Reproduce

  1. Create a Svelte component wrapped in an Astro component
  2. Use any client directive: client:load, client:idle, client:only="svelte", or client:media
  3. Build the site and deploy
  4. Run Lighthouse performance test
  5. Observe Network Dependency Tree in Lighthouse report

Expected Behavior

With client:load, the component's JavaScript should:

  • Not be included in the initial HTML response
  • Start downloading after the window load event fires
  • Not appear in Lighthouse's critical rendering path
  • Not impact LCP measurement

With client:only, the component should:

  • Skip server-side rendering (this works)
  • Have its JavaScript completely deferred from initial bundle
  • Load asynchronously after page is interactive

Actual Behavior

All client directives include the component's JavaScript in the initial page bundle:

Network Dependency Tree (from Lighthouse):

Initial Navigation
https://vashonmesh.org - 76 ms, 28.31 KiB
├── /_astro/FloatingImages.DIgMZlMV.js - 247 ms, 2.57 KiB
├── /_astro/client.svelte.CX8gQjlJ.js - 247 ms, 0.67 KiB
├── /_astro/template.Cwu_fZMk.js - 504 ms, 8.62 KiB
├── /_astro/render.BvwD2nt8.js - 503 ms, 2.96 KiB
└── /_astro/lifecycle.DoIy0IOf.js - 503 ms, 2.22 KiB

Result:

  • LCP increased from 1.2s → 8.3s
  • Component JavaScript in critical path despite client:only="svelte"
  • No apparent way to defer loading until after LCP measurement

Tested Workarounds

Attempt 1: client:idle

<FloatingImagesClient client:idle />
  • Result: Scripts load before window load event
  • LCP Impact: 8.9s

Attempt 2: client:load

<FloatingImagesClient client:load />
  • Result: Scripts still bundled in initial HTML, just delayed execution
  • LCP Impact: 3.0s

Attempt 3: client:only="svelte"

<FloatingImagesClient client:only="svelte" />
  • Result: Skips SSR but JavaScript still in critical path
  • LCP Impact: 8.3s

Attempt 4: Manual Chunk Splitting

// astro.config.mjs
build: {
  rollupOptions: {
    output: {
      manualChunks: (id) => {
        if (id.includes("FloatingImages")) return "floating-images";
      };
    }
  }
}
  • Result: Build errors (missing Zod dependencies from Astro internals)

Attempt 5: requestIdleCallback in Svelte

onMount(() => {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(startAnimation, { timeout: 2000 });
  }
});
  • Result: Only delays animation start, not JavaScript loading
  • LCP Impact: Still 8.3s (scripts loaded early)

Code Example

FloatingImages.astro:

---
import FloatingImagesClient from "./FloatingImages.svelte";
const { imageCount = 12 } = Astro.props;
// ... image optimization code ...
---

<FloatingImagesClient
  client:only="svelte"
  floatingImageItems={floatingImageItems}
  keyframesCss={keyframesCss}
/>

Layout.astro:

{floatingImages && <FloatingImages imageCount={imageCount} />}

Environment

  • Astro: 6.0.0-beta.14
  • Svelte Integration: @astrojs/svelte
  • Build Tool: Vite
  • Node: 22.12.0
  • Package Manager: pnpm

Desired Solution

One of:

  1. New directive: client:defer that:

    • Completely excludes component from initial bundle
    • Loads JavaScript only after window load event + requestIdleCallback
    • Does not appear in Lighthouse critical rendering path
  2. Enhanced client:load:

    • Should truly defer loading until after window load
    • Should not bundle scripts in initial HTML response
    • Should use dynamic import at runtime
  3. Configuration option:

    <Component client:load={{ defer: true, priority: "low" }} />
  4. Documentation: If this is by design, document how to achieve true deferred loading for non-critical components

Impact

This limitation makes it impossible to have decorative/non-essential components that don't impact Core Web Vitals (LCP, TBT). For sites focused on performance scores (Google PageSpeed, Lighthouse), this forces removal of visual enhancements.

Temporary Solution

Currently, we've completely disabled the component:

<!-- FloatingImages temporarily disabled to fix LCP --><!-- TODO: Need Astro support for truly deferred component loading -->

This is not ideal as the feature adds visual interest to the site.

References

Additional Context

Our memory notes from multiple debugging sessions:

- ANY hydration directive except `client:only` adds JS to initial bundle (proven false - client:only also bundles)
- `client:idle` fires too early, before window load
- `client:load` waits for window load BUT still bundles scripts in initial HTML
- Manual chunk splitting causes build errors (missing Zod dependencies)
- Dynamic imports of Astro components not supported at runtime

We've spent considerable time trying every available approach and conclude this is a framework limitation rather than implementation error.

What's the expected result?

See above

Link to Minimal Reproducible Example

See main bug report

Participation

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs triageIssue needs to be triaged

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions