Skip to content

feat:Actually ship no JS by default#3696

Merged
bartlomieju merged 11 commits into
freshframework:mainfrom
Ionaru:actually-no-js-by-default
Mar 29, 2026
Merged

feat:Actually ship no JS by default#3696
bartlomieju merged 11 commits into
freshframework:mainfrom
Ionaru:actually-no-js-by-default

Conversation

@Ionaru

@Ionaru Ionaru commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

In many places, Fresh marketing claims the framework ships no JS by default, this was false.

This PR reworks core parts of the framework to check whether JS is needed on the client with these criteria:

  1. Use of islands
  2. Use of Partials (f-client-nav)

If either of those those criteria are met, JavaScript will be loaded as normal, but if both are false, then the client-entry script is omitted from the page.

Fixes #3662

Ionaru added 4 commits March 21, 2026 14:53
- Introduced `hmrClientEntry` to the `BuildCache` interface for development mode.
- Updated the `Context` class to conditionally add preload headers based on client runtime needs.
- Moved HMR client logic in `dev_hmr.ts`.
- Enhanced the `RenderState` to track client navigation requirements and runtime needs.
- Added tests to ensure no client JS is emitted for static pages without islands or client navigation.
Comment thread deno.lock

@bartlomieju bartlomieju 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.

See comment about the lockfile

@Ionaru

Ionaru commented Mar 24, 2026

Copy link
Copy Markdown
Contributor Author

I could not run deno install --frozen without updating the lockfile.
Probably an issue on the main branch, but I'll revert that.

@bartlomieju bartlomieju 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.

Nice work — this fixes a real gap between Fresh's marketing ("zero JS by default") and reality. The approach of checking islands.size > 0 || clientNavEnabled is clean, and the HMR split is a good call so dev mode still gets live reload on static pages.

A few things to consider:

Logic issue in FreshRuntimeScript: After the PR, when needsClientRuntime is false, the function falls through past the if (RENDER_STATE!.needsClientRuntime) block to the HMR/production-static returns at the bottom. But when needsClientRuntime is true, the full-document branch already returns inside the block — so that's fine. However, the partial response path (line ~582) returns early and doesn't check needsClientRuntime at all. That seems correct for partials, but worth a comment noting that partials always assume the client runtime is already loaded by the parent page.

f-client-nav detection only checks "true" string: The diff hook checks (vnode.props)[CLIENT_NAV_ATTR] === "true", but earlier in the same file (line ~145-146), the attribute is coerced via String(vnode.props[CLIENT_NAV_ATTR]). So passing f-client-nav as a boolean true (JSX f-client-nav or f-client-nav={true}) gets stringified to "true" before this check runs — that works. But the test uses <html f-client-nav> which in Preact/JSX becomes true (boolean). Worth verifying this ordering is guaranteed (diff hook runs after vnode hook), and maybe add a brief comment explaining the dependency.

f-client-nav={false} test: The test asserts no boot script, which is correct. But f-client-nav={false} still gets stringified to "false" and rendered into the HTML as f-client-nav="false". The detection correctly skips it since "false" !== "true". Good.

Minor: The isJsMediaType exhaustive return at the end of deno.ts — this is unrelated to the feature but fine as a drive-by fix for the type checker.

Overall this looks good to merge once the ordering guarantee between the vnode hook and diff hook is confirmed/documented.

Comment thread packages/fresh/src/runtime/server/preact_hooks.ts
@Ionaru

Ionaru commented Mar 26, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the review! I'll process your feedback when I've got some time in the next couple of days.

Ionaru added 3 commits March 29, 2026 15:15
Partials browser tests failed because full-document responses no longer
emitted the Fresh boot script when pages used <Partial> without reliable
f-client-nav detection under jsx precompile. Treat any non–partial-request
document that renders <Partial> as needing the client runtime.

Vite dev Tailwind tests failed because the HMR-only script path never loaded
the client entry (and thus CSS side-effect imports). Emit the full inline
boot whenever hmrClientEntry is set, and align Link modulepreload headers.

Update Tailwind dev server tests to accept Vite 7’s vite-module-id on
injected styles in addition to data-vite-dev-id.
@Ionaru

Ionaru commented Mar 29, 2026

Copy link
Copy Markdown
Contributor Author

@bartlomieju Comments added and tests fixed :)

@bartlomieju bartlomieju changed the title feat: *Actually* ship no JS by default feat:Actually ship no JS by default Mar 29, 2026
The partial branch already returns early, so the second if was
unreachable for partial responses. Using else-if makes this
mutual exclusivity explicit to future readers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@bartlomieju bartlomieju 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.

Nice work, thank you

@bartlomieju bartlomieju enabled auto-merge (squash) March 29, 2026 20:01
@bartlomieju bartlomieju merged commit 6dd2c97 into freshframework:main Mar 29, 2026
7 checks passed
@Ionaru Ionaru deleted the actually-no-js-by-default branch March 29, 2026 22:52
@jeveloper

Copy link
Copy Markdown

yahooooo

:))

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.

No JavaScript?

3 participants