Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async Rendering: A new approach ⚡ #742

Merged
merged 9 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"std/": "https://deno.land/[email protected]/",
"partytown/": "https://deno.land/x/[email protected]/",
"deco-sites/std/": "https://denopkg.com/deco-sites/[email protected]/",
"deco/": "https://cdn.jsdelivr.net/gh/deco-cx/deco@1.78.0/"
"deco/": "https://cdn.jsdelivr.net/gh/deco-cx/deco@1.80.0/"
},
"lock": false,
"tasks": {
Expand Down
8 changes: 5 additions & 3 deletions htmx/sections/Deferred.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Section } from "deco/blocks/section.ts";
import { useSection } from "deco/hooks/useSection.ts";
import { asResolved, isDeferred } from "deco/mod.ts";
import { shouldForceRender } from "../../utils/deferred.ts";
import { AppContext } from "../mod.ts";
import { renderSection, shouldForceRender } from "../../utils/deferred.tsx";

/**
* @titleBy type
Expand Down Expand Up @@ -46,7 +46,7 @@ const Deferred = (props: Props) => {
if (loading === "eager") {
return (
<>
{sections.map(renderSection)}
{sections.map((section) => <section.Component {...section.props} />)}
</>
);
}
Expand All @@ -69,7 +69,9 @@ const Deferred = (props: Props) => {
hx-swap="outerHTML"
style={{ height: "100vh" }}
/>
{props.fallbacks?.map(renderSection)}
{props.fallbacks?.map((section) =>
section ? <section.Component {...section.props} /> : null
)}
</>
);
};
Expand Down
7 changes: 1 addition & 6 deletions utils/deferred.tsx → utils/deferred.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { Section } from "deco/mod.ts";
import { __DECO_FBT } from "../website/handlers/fresh.ts";
export const __DECO_FBT = "__decoFBT";

export const shouldForceRender = <Ctx extends { isBot?: boolean }>(
{ ctx, searchParams }: { ctx: Ctx; searchParams: URLSearchParams },
): boolean => ctx.isBot || searchParams.get(__DECO_FBT) === "0";

export const renderSection = ({ Component, props }: Section) => (
<Component {...props} />
);
3 changes: 1 addition & 2 deletions website/handlers/fresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import { DecoState } from "deco/types.ts";
import { allowCorsFor } from "deco/utils/http.ts";
import { getSetCookies } from "std/http/cookie.ts";
import { ConnInfo } from "std/http/server.ts";
import { __DECO_FBT } from "../../utils/deferred.ts";
import { AppContext } from "../mod.ts";

export const __DECO_FBT = "__decoFBT";

/**
* @title Fresh Config
*/
Expand Down
14 changes: 8 additions & 6 deletions website/manifest.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ import * as $$$$$$$14 from "./matchers/userAgent.ts";
import * as $$$$$0 from "./pages/Page.tsx";
import * as $$$$$$0 from "./sections/Analytics/Analytics.tsx";
import * as $$$$$$1 from "./sections/Rendering/Deferred.tsx";
import * as $$$$$$2 from "./sections/Rendering/SingleDeferred.tsx";
import * as $$$$$$3 from "./sections/Seo/Seo.tsx";
import * as $$$$$$4 from "./sections/Seo/SeoV2.tsx";
import * as $$$$$$2 from "./sections/Rendering/Lazy.tsx";
import * as $$$$$$3 from "./sections/Rendering/SingleDeferred.tsx";
import * as $$$$$$4 from "./sections/Seo/Seo.tsx";
import * as $$$$$$5 from "./sections/Seo/SeoV2.tsx";

const manifest = {
"functions": {
Expand Down Expand Up @@ -84,9 +85,10 @@ const manifest = {
"sections": {
"website/sections/Analytics/Analytics.tsx": $$$$$$0,
"website/sections/Rendering/Deferred.tsx": $$$$$$1,
"website/sections/Rendering/SingleDeferred.tsx": $$$$$$2,
"website/sections/Seo/Seo.tsx": $$$$$$3,
"website/sections/Seo/SeoV2.tsx": $$$$$$4,
"website/sections/Rendering/Lazy.tsx": $$$$$$2,
"website/sections/Rendering/SingleDeferred.tsx": $$$$$$3,
"website/sections/Seo/Seo.tsx": $$$$$$4,
"website/sections/Seo/SeoV2.tsx": $$$$$$5,
},
"matchers": {
"website/matchers/always.ts": $$$$$$$0,
Expand Down
5 changes: 3 additions & 2 deletions website/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ export interface Props {
caching?: Caching;

/**
* @title Async Rendering
* @description Async sections will be deferred to the client-side
* @title Global Async Rendering (Deprecated)
* @description Please disable this setting and enable each section individually. More info at https://deco.cx/en/blog/async-render-default
* @deprecated true
* @default false
*/
firstByteThresholdMS?: boolean;
Expand Down
7 changes: 2 additions & 5 deletions website/sections/Rendering/Deferred.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useScriptAsDataURI } from "deco/hooks/useScript.ts";
import { asResolved, isDeferred } from "deco/mod.ts";
import { useId } from "preact/hooks";
import { AppContext } from "../../mod.ts";
import { renderSection, shouldForceRender } from "../../../utils/deferred.tsx";
import { shouldForceRender } from "../../../utils/deferred.ts";

/** @titleBy type */
export interface Scroll {
Expand Down Expand Up @@ -43,8 +43,6 @@ export interface Props {
sections: Section[];
display?: boolean;
behavior?: Scroll | Intersection | Load;
/** @hide true */
fallbacks?: Section[];
}

const script = (
Expand Down Expand Up @@ -103,7 +101,7 @@ const Deferred = (props: Props) => {
if (display) {
return (
<>
{sections.map(renderSection)}
{sections.map(({ Component, props }) => <Component {...props} />)}
</>
);
}
Expand All @@ -125,7 +123,6 @@ const Deferred = (props: Props) => {
behavior?.payload.toString() || "",
)}
/>
{props.fallbacks?.map(renderSection)}
</>
);
};
Expand Down
157 changes: 157 additions & 0 deletions website/sections/Rendering/Lazy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import type { Section } from "deco/blocks/section.ts";
import { SectionContext } from "deco/components/section.tsx";
import { asResolved, context, isDeferred } from "deco/mod.ts";
import { useContext } from "preact/hooks";
import { shouldForceRender } from "../../../utils/deferred.ts";
import type { AppContext } from "../../mod.ts";

const useSectionContext = () => useContext(SectionContext);

interface Props {
/** @label hidden */
section: Section;

/**
* @description htmx/Deferred.tsx prop
* @hide true
*/
loading?: "eager" | "lazy";
}

const defaultFallbackFor = (section: string) => () => (
<div
style={{
height: "50vh",
width: "100%",
display: "flex",
flexDirection: "column",
gap: "0.5rem",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
}}
>
{!context.isDeploy && (
<>
<div>
Async Rendering not implemented for section{" "}
<span style={{ fontSize: "1rem", fontWeight: 600 }}>{section}</span>
</div>
<div style={{ fontSize: "0.75rem" }}>
If you are a developer, export a{" "}
<span style={{ fontWeight: 600 }}>LoadingFallback</span>{" "}
component in this section. To learn more, check out our{" "}
<a
style={{ fontWeight: 600, textDecoration: "underline" }}
href="https://deco.cx/en/blog/async-rendering#:~:text=Customizing%20Loading%20States%20Made%20Easy%3A"
>
guide
</a>
</div>
<div style={{ fontSize: "0.75rem" }}>
If you are NOT a developer, you can tweak Async Rendering. To learn
more, check out our{" "}
<a
style={{ fontWeight: 600, textDecoration: "underline" }}
href="https://deco.cx/en/blog/async-render-default#:~:text=Q%3A%20Can%20I%20disable%20the%20async%20render%3F"
>
blog post
</a>
</div>
</>
)}
</div>
);

export const loader = async (props: Props, req: Request, ctx: AppContext) => {
const { section, loading } = props;
const url = new URL(req.url);

const shouldRender = loading === "eager" || shouldForceRender({
ctx,
searchParams: url.searchParams,
});

if (shouldRender) {
return {
loading: "eager",
section: isDeferred<Section>(section) ? await section() : section,
};
}

const resolvingMatchers: Record<string, boolean> = {};
const resolvedSection = isDeferred<Section>(section)
? await section({}, {
propagateOptions: true,
hooks: {
onResolveStart: (resolve, _props, resolver, _resolveType, ctx) => {
if (ctx.resolveId in resolvingMatchers) {
return resolve();
}
if (resolver?.type === "loaders") {
// deno-lint-ignore no-explicit-any
return undefined as any;
}
return resolve();
},
onPropsResolveStart: (
resolve,
_props,
resolver,
_resolveType,
ctx,
) => {
if (ctx.resolveId in resolvingMatchers) {
return resolve();
}
if (resolver?.type === "matchers") { // matchers should not have a timeout.
const id = crypto.randomUUID();
resolvingMatchers[id] = true;
return resolve(id);
}
if (resolver?.type === "loaders") {
return Promise.resolve([]);
}
return resolve();
},
},
})
: section;

return {
loading: shouldRender ? "eager" : "lazy",
section: {
Component: resolvedSection.LoadingFallback ??
defaultFallbackFor(resolvedSection.metadata?.component ?? "unknown"),
metadata: resolvedSection.metadata,
props: {},
},
};
};

type SectionProps = Awaited<ReturnType<typeof loader>>;

function Lazy({ section, loading }: SectionProps) {
const ctx = useSectionContext();

if (!ctx) {
throw new Error("Missing SectionContext");
}

if (loading === "lazy") {
return (
<ctx.FallbackWrapper props={{ loading: "eager" }}>
<section.Component {...section.props} />
</ctx.FallbackWrapper>
);
}

return <section.Component {...section.props} />;
}

export const onBeforeResolveProps = (props: Props) => ({
...props,
section: asResolved(props.section, true),
});

export default Lazy;
Loading
Loading