# 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.

Compare NetScript with  Next.js Nuxt SvelteKit TanStack Start

**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.

```tsx
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;
```

```tsx
// 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.
```

```text
<!-- 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>
```

```text
// 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>
```

```tsx
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:

```tsx
.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% |

[Comparisons](https://rickylabs.github.io/netscript/netscript/comparisons/) [Backend frameworks](https://rickylabs.github.io/netscript/netscript/comparisons/backend/)
