Skip to content

Commit

Permalink
Async Rendering: A new approach ⚡ (#742)
Browse files Browse the repository at this point in the history
* async rendering

* async rendering

* Fix loading loaders on resolveStart

Signed-off-by: Marcos Candeia <[email protected]>

* Do not resolve loaders props

Signed-off-by: Marcos Candeia <[email protected]>

* Fix nul check

Signed-off-by: Marcos Candeia <[email protected]>

* Do not invoke loaders

Signed-off-by: Marcos Candeia <[email protected]>

* Resolve empty props

Signed-off-by: Marcos Candeia <[email protected]>

* Rollback deferred to old version

Signed-off-by: Marcos Candeia <[email protected]>

* Fix deferred checks

Signed-off-by: Marcos Candeia <[email protected]>

---------

Signed-off-by: Marcos Candeia <[email protected]>
Co-authored-by: Marcos Candeia <[email protected]>
  • Loading branch information
tlgimenes and mcandeia authored Jul 25, 2024
1 parent c00d746 commit df14eb9
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 132 deletions.
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

0 comments on commit df14eb9

Please sign in to comment.