NetScript vs frontend frameworks
NetScript makes the whole page legible at its entry point
The route, shared read, two streamed regions, skeletons, layout, refresh decision, and metadata are one typed chain. Pick a competitor; the NetScript side stays put because the argument does too.
Next.js: React Server Components stream well, but the page contract is distributed across the page, parallel-route slots, loading and error files, metadata, and cache declarations.
Nuxt: Nuxt keeps this compact in one Vue page and shares keyed async data cleanly; its lazy regions become client loading states, so the refresh and region policy stay imperative.
SvelteKit: SvelteKit streams unresolved server-load promises with very little ceremony; the boundary is split between the server load and the page component, and client invalidation owns refresh.
TanStack Start: TanStack Start gives streamed promises and typed route data a strong home; caching, head metadata, Suspense resolution, and error boundaries remain separate concepts you assemble.
import { definePage } from '@netscript/fresh/builders';
import { appRoutes } from '../../.netscript/routes.ts';
import { ProductLayout } from '../../components/ProductLayout.tsx';
import { Overview, OverviewSkeleton } from '../../components/Overview.tsx';
import { Stock, StockSkeleton } from '../../components/Stock.tsx';
const page = definePage()
.withRoute(appRoutes.products.$product.$route)
.withStreaming()
.withResource('product', (ctx) => {
const product = productCache.require(ctx.path.product);
void productCache.revalidate(ctx.path.product);
return product;
})
.withLayer('overview', Overview, {
loader: async (ctx) => ({
product: await ctx.resource('product'),
reviews: await loadReviews(ctx.path.product),
}),
fallback: <OverviewSkeleton />,
delivery: 'stream',
})
.withLayer('stock', Stock, {
loader: async (ctx) => ({
product: await ctx.resource('product'),
stock: await loadStock(ctx.path.product),
}),
fallback: <StockSkeleton />,
delivery: 'stream',
})
.withLayout((slots) => (
<ProductLayout overview={slots.overview()} stock={slots.stock()} />
))
.withMeta(async (ctx) => {
const product = await ctx.resource('product');
return { title: product.name, description: product.summary };
})
.build();
export default page.default;
// app/products/[product]/data.ts
import { cache } from 'react';
export const getProduct = cache((id: string) =>
fetch(`${API}/products/${id}`, { next: { revalidate: 30 } }).then(r => r.json())
);
// app/products/[product]/page.tsx
export const generateMetadata = async ({ params }: PageProps<'/products/[product]'>) => {
const { product } = await params;
const item = await getProduct(product);
return { title: item.name, description: item.summary };
};
const Page = ({ overview, stock }: {
overview: React.ReactNode; stock: React.ReactNode;
}) => <ProductLayout overview={overview} stock={stock} />;
export default Page;
// app/products/[product]/@overview/page.tsx
const OverviewSlot = async ({ params }: PageProps<'/products/[product]'>) => {
const { product } = await params;
return <Overview product={await getProduct(product)} reviews={await loadReviews(product)} />;
};
export default OverviewSlot;
// app/products/[product]/@stock/page.tsx
const StockSlot = async ({ params }: PageProps<'/products/[product]'>) => {
const { product } = await params;
return <Stock product={await getProduct(product)} stock={await loadStock(product)} />;
};
export default StockSlot;
// @overview/loading.tsx and @stock/loading.tsx export the skeletons.
// Each slot adds error.tsx when its failure must stay local.<!-- app/pages/products/[product].vue -->
<script setup lang='ts'>
const route = useRoute();
const key = `product:${route.params.product}`;
const { data: product, refresh } = await useAsyncData(
key,
() => $fetch(`/api/products/${route.params.product}`),
);
const { data: reviews, status: reviewsStatus } = useLazyAsyncData(
`${key}:reviews`,
() => $fetch(`/api/products/${route.params.product}/reviews`),
);
const { data: stock, status: stockStatus } = useLazyAsyncData(
`${key}:stock`,
() => $fetch(`/api/products/${route.params.product}/stock`),
);
useSeoMeta({
title: () => product.value?.name,
description: () => product.value?.summary,
});
let timer: ReturnType<typeof setInterval>;
onMounted(() => { timer = setInterval(refresh, 30_000); });
onUnmounted(() => clearInterval(timer));
</script>
<template>
<ProductLayout>
<OverviewSkeleton v-if='reviewsStatus === `pending`' />
<Overview v-else :product :reviews />
<StockSkeleton v-if='stockStatus === `pending`' />
<Stock v-else :product :stock />
</ProductLayout>
</template>// routes/products/[product]/+page.server.ts
export const load = async ({ params, depends }) => {
depends(`product:${params.product}`);
const product = await getProduct(params.product);
return {
product,
reviews: loadReviews(params.product),
stock: loadStock(params.product),
};
};
<!-- routes/products/[product]/+page.svelte -->
<script lang='ts'>
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
let { data } = $props();
onMount(() => {
const timer = setInterval(() => invalidate(`product:${data.product.id}`), 30_000);
return () => clearInterval(timer);
});
</script>
<svelte:head><title>{data.product.name}</title></svelte:head>
<ProductLayout>
{#await data.reviews}<OverviewSkeleton />{:then reviews}<Overview product={data.product} {reviews} />{/await}
{#await data.stock}<StockSkeleton />{:then stock}<Stock product={data.product} {stock} />{/await}
</ProductLayout>import { Await, createFileRoute } from '@tanstack/react-router';
import { queryOptions } from '@tanstack/react-query';
const productOptions = (id: string) => queryOptions({
queryKey: ['product', id],
queryFn: () => getProduct(id),
staleTime: 30_000,
});
export const Route = createFileRoute('/products/$product')({
loader: async ({ params, context }) => {
const product = await context.queryClient.ensureQueryData(
productOptions(params.product),
);
return {
product,
reviews: loadReviews(params.product),
stock: loadStock(params.product),
};
},
head: ({ loaderData }) => ({
meta: [{ title: loaderData?.product.name }],
}),
component: ProductPage,
});
const ProductPage = () => {
const { product, reviews, stock } = Route.useLoaderData();
return <ProductLayout>
<Await promise={reviews} fallback={<OverviewSkeleton />}>
{(value) => <Overview product={product} reviews={value} />}
</Await>
<Await promise={stock} fallback={<StockSkeleton />}>
{(value) => <Stock product={product} stock={value} />}
</Await>
</ProductLayout>;
};NetScript can say who owns the I/O in one line
When a region has no authoritative cache timestamp, make its typed partial own the read and keep the page out of its critical path:
.withLayer("livePrice", LivePrice, {
loader: () => undefined,
partial: (ctx) => appRoutes.productPricePartial.href({ path: ctx.path }),
partialName: "product-price",
fallback: <PriceSkeleton />,
staleTime: 30_000,
staleReloadMode: "background",
})
NetScript exposes more of the architecture where you enter the route
These are architectural estimates for this generic product page, not benchmark results.
| Architectural estimate | NetScript composition | Convention-split equivalent |
|---|---|---|
| Route-specific orchestration | ~175 LOC | ~190–225 LOC |
| Files needed to see page behaviour | 1 | ~5–8 |
| Architectural surface visible at entry | ~87–93% | ~40–53% |