Skip to main content
0.0.x

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.

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.

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%