{"id":238,"date":"2026-06-19T19:09:52","date_gmt":"2026-06-19T11:09:52","guid":{"rendered":"https:\/\/erishen.cn\/?page_id=238"},"modified":"2026-06-19T19:27:51","modified_gmt":"2026-06-19T11:27:51","slug":"building-production-react-ssr-framework","status":"publish","type":"page","link":"https:\/\/erishen.cn\/?page_id=238","title":{"rendered":"Building a Production-Grade React SSR Framework from Scratch"},"content":{"rendered":"<p><\/p>\n<h2>Introduction<\/h2>\n<p><\/p>\n<p>In the React ecosystem, Next.js stands as the representative SSR framework, beloved by developers for its out-of-the-box experience and powerful features. But for those who want <strong>complete control over the build process<\/strong>, <strong>deep customization of deployment strategies<\/strong>, or simply <strong>understanding SSR fundamentals<\/strong>, Next.js&#8217;s &#8220;black box&#8221; nature becomes a limitation.<\/p>\n<p><\/p>\n<p>I spent several months building a lightweight yet fully-featured React SSR framework from scratch \u2014 <strong>NSBP (Node React SSR by Webpack)<\/strong>. Here&#8217;s the development journey, technical choices, core features, and practical experience gained.<\/p>\n<p><\/p>\n<p><strong>Live Demo<\/strong>: <a href=\"https:\/\/nsbp.erishen.cn\/\">https:\/\/nsbp.erishen.cn\/<\/a> | <strong>GitHub<\/strong>: <a href=\"https:\/\/github.com\/erishen\/nsbp\">https:\/\/github.com\/erishen\/nsbp<\/a><\/p>\n<p><\/p>\n<h2>Why Build Instead of Using Next.js?<\/h2>\n<p><\/p>\n<h3>Complete Control Over the Build Process<\/h3>\n<p><\/p>\n<p>Next.js&#8217;s power comes from automation, but that means losing some control:<\/p>\n<p><\/p>\n<pre><code>\n\/\/ Next.js build (black box)\nnext build\n\/\/ \u2192 auto routing, auto code splitting, auto image optimization, hundreds of hidden steps\n\n\/\/ NSBP \u2014 precise control over every step\n\/\/ config\/webpack.base.js\nmodule.exports = {\n  entry: {\n    client: '.\/src\/client\/index.tsx',\n    server: '.\/src\/server\/index.ts'\n  },\n  plugins: [\n    new MiniCssExtractPlugin(),\n    new TerserPlugin(),\n    \/\/ add any Webpack plugin\n  ]\n}\n<\/code><\/pre>\n<p><\/p>\n<h3>Deep Customization of Deployment<\/h3>\n<p><\/p>\n<p>Next.js&#8217;s Vercel deployment is convenient, but if you want to deploy on your own Linux server, use Docker Compose, configure reverse proxies and load balancing, or implement custom security policies \u2014 controlling your own Express server is advantageous.<\/p>\n<p><\/p>\n<h3>Learning SSR Fundamentals<\/h3>\n<p><\/p>\n<p>Writing SSR rendering logic by hand gives you deep understanding:<\/p>\n<p><\/p>\n<pre><code>\nconst sheet = new ServerStyleSheet()\nconst jsx = sheet.collectStyles(\n  &lt;Provider store={store}&gt;\n    &lt;StaticRouter location={reqPath}&gt;\n      &lt;Routes&gt;{\/* ... *\/}&lt;\/Routes&gt;\n    &lt;\/StaticRouter&gt;\n  &lt;\/Provider&gt;\n)\nconst content = renderToString(jsx)\nconst styleTags = sheet.getStyleTags()\n<\/code><\/pre>\n<p><\/p>\n<h2>Technical Stack &#038; Architecture<\/h2>\n<p><\/p>\n<table>\n<thead>\n<tr>\n<td>Category<\/td>\n<td>Choice<\/td>\n<td>Version<\/td>\n<td>Reason<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>React<\/td>\n<td>React 19<\/td>\n<td>19.2.3<\/td>\n<td>Latest, concurrent features, better type support<\/td>\n<\/tr>\n<tr>\n<td>State<\/td>\n<td>Redux Toolkit<\/td>\n<td>\u2014<\/td>\n<td>Simplified Redux, built-in Thunk support<\/td>\n<\/tr>\n<tr>\n<td>Router<\/td>\n<td>React Router DOM<\/td>\n<td>7.12.0<\/td>\n<td>Stable client-side routing<\/td>\n<\/tr>\n<tr>\n<td>Code Split<\/td>\n<td>@loadable\/component<\/td>\n<td>5.15.0<\/td>\n<td>Mature code splitting<\/td>\n<\/tr>\n<tr>\n<td>Build<\/td>\n<td>Webpack 5<\/td>\n<td>5.96.0<\/td>\n<td>Modular, extensible, rich ecosystem<\/td>\n<\/tr>\n<tr>\n<td>Server<\/td>\n<td>Express<\/td>\n<td>5.2.1<\/td>\n<td>Lightweight, rich middleware ecosystem<\/td>\n<\/tr>\n<tr>\n<td>Styling<\/td>\n<td>Styled-components + Sass\/Less<\/td>\n<td>\u2014<\/td>\n<td>Multiple choices, flexible switching<\/td>\n<\/tr>\n<tr>\n<td>Language<\/td>\n<td>TypeScript<\/td>\n<td>5.x<\/td>\n<td>Type safety, excellent DX<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/p>\n<h3>SSR Rendering Architecture<\/h3>\n<p><\/p>\n<pre><code>\nHTTP Request\n    \u2193\nExpress Middleware (Helmet, Rate Limit)\n    \u2193\nSSR Render Layer:\n  1. Create Redux Store\n  2. Prefetch route data (loadData)\n  3. renderToString \u2192 HTML\n  4. Extract Styled-components styles\n  5. Serialize state to window.context\n    \u2193\nReturn HTML + Initial State\n<\/code><\/pre>\n<p><\/p>\n<h3>Client Hydration<\/h3>\n<p><\/p>\n<pre><code>\nconst App = () =&gt; {\n  const store = useMemo(() =&gt; {\n    if (isSEO() &amp;&amp; window?.context?.state) {\n      return getStore(window.context.state)\n    }\n    return getStore()\n  }, [])\n\n  return (\n    &lt;Provider store={store}&gt;\n      &lt;BrowserRouter&gt;\n        &lt;Routes&gt;{\/* ... *\/}&lt;\/Routes&gt;\n      &lt;\/BrowserRouter&gt;\n    &lt;\/Provider&gt;\n  )\n}\n\nloadableReady(() =&gt; {\n  hydrateRoot(document.getElementById('root')!, &lt;App \/&gt;)\n})\n<\/code><\/pre>\n<p><\/p>\n<h2>Core Features<\/h2>\n<p><\/p>\n<h3>Three Rendering Modes<\/h3>\n<p><\/p>\n<table>\n<thead>\n<tr>\n<td>Mode<\/td>\n<td>URL<\/td>\n<td>Use Case<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SSR (default)<\/td>\n<td><code>\/<\/code> or <code>\/?nsbp=1<\/code><\/td>\n<td>SEO, fast initial load<\/td>\n<\/tr>\n<tr>\n<td>CSR<\/td>\n<td><code>\/?nsbp=0<\/code><\/td>\n<td>Debugging, dev experience<\/td>\n<\/tr>\n<tr>\n<td>SSR Fallback<\/td>\n<td><code>\/?nsbp=1&from=link<\/code><\/td>\n<td>Internal links, allow client update<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/p>\n<h3>Smart SSR Data Prefetching<\/h3>\n<p><\/p>\n<p>Server-side data prefetch to avoid client-side re-requests:<\/p>\n<p><\/p>\n<pre><code>\n\/\/ src\/Routers.tsx\nexport default [{\n  path: '\/',\n  element: &lt;Home \/&gt;,\n  loadData: homeLoadData,  \/\/ \u2190 prefetch on SSR\n  key: 'home'\n}]\n<\/code><\/pre>\n<p><\/p>\n<p><strong>Performance comparison<\/strong>: Traditional CSR ~2-3 seconds; NSBP SSR ~0.5-1 second.<\/p>\n<p><\/p>\n<h3>Code Splitting &#038; Lazy Loading<\/h3>\n<p><\/p>\n<pre><code>\nimport loadable from '@loadable\/component'\nconst Home = loadable(() =&gt; import('@containers\/Home'), { fallback: &lt;Loading \/&gt; })\n<\/code><\/pre>\n<p><\/p>\n<p>Initial load: ~200KB main bundle, only home page code. Other pages loaded on demand.<\/p>\n<p><\/p>\n<h3>Multi-Layer Security<\/h3>\n<p><\/p>\n<p>&#8211; <strong>Helmet security headers<\/strong>: CSP, X-Frame-Options, X-Content-Type-Options, HSTS<\/p>\n<p>&#8211; <strong>Rate limiting<\/strong>: 100 requests per 15 minutes on API routes<\/p>\n<p><\/p>\n<h3>Docker One-Click Deployment<\/h3>\n<p><\/p>\n<pre><code>\n# docker-compose.yml\nservices:\n  app:\n    build:\n      context: .\n      dockerfile: docker\/Dockerfile\n    ports: [\"8081:3001\"]\n    environment: [NODE_ENV=production, ENABLE_RATE_LIMIT=1]\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"node\", \"-e\", \"require('http').get(...)\"]\n<\/code><\/pre>\n<p><\/p>\n<h2>Developer Experience<\/h2>\n<p><\/p>\n<p>&#8211; <strong>Quality toolchain<\/strong>: ESLint + Prettier + Husky pre-commit hooks with lint-staged<\/p>\n<p>&#8211; <strong>CLI tool<\/strong>: <code>npx nsbp-cli create my-app<\/code> for project scaffolding<\/p>\n<p>&#8211; <strong>Make commands<\/strong>: <code>make dev<\/code>, <code>make prod<\/code>, <code>make logs<\/code>, <code>make shell<\/code><\/p>\n<p><\/p>\n<h2>Performance Optimizations<\/h2>\n<p><\/p>\n<p>&#8211; <strong>Tree shaking<\/strong>: <code>usedExports: true<\/code>, <code>sideEffects: false<\/code><\/p>\n<p>&#8211; <strong>Code minification<\/strong>: TerserPlugin with multi-process compression, <code>drop_console<\/code> in production<\/p>\n<p>&#8211; <strong>Static caching<\/strong>: 1-year immutable cache for JS\/CSS\/images<\/p>\n<p>&#8211; <strong>Image dimension probing<\/strong>: probe-image-size during SSR to avoid client re-requests<\/p>\n<p><\/p>\n<h2>When to Use (and Not Use) NSBP<\/h2>\n<p><\/p>\n<p><strong>Good for<\/strong>: personal projects, rapid prototypes, education\/learning, highly customized projects, low-resource environments<\/p>\n<p><\/p>\n<p><strong>Not ideal for<\/strong>: large enterprise apps, rapid iteration projects, AI\/ML features<\/p>\n<p><\/p>\n<h2>Problems Solved<\/h2>\n<p><\/p>\n<p>&#8211; <strong>React 19 Hydration Error #418<\/strong>: Replaced <code>dangerouslySetInnerHTML<\/code> scripts with <code>useEffect<\/code><\/p>\n<p>&#8211; <strong>Docker port config<\/strong>: Moved docker-compose.yml to root directory for .env access<\/p>\n<p>&#8211; <strong>Webpack Dev Server hot reload<\/strong>: Correctly configured Browser Sync Webpack Plugin<\/p>\n<p><\/p>\n<h2>Future Plans<\/h2>\n<p><\/p>\n<p>React Server Components, tRPC integration, Vitest migration, Service Worker caching, CDN support, ISR implementation<\/p>\n<p><\/p>\n<h2>Conclusion<\/h2>\n<p><\/p>\n<p>Building NSBP was an invaluable learning experience. While it may not replace Next.js, I deeply understood every link in modern frontend engineering, mastered building complete applications from scratch, and developed problem-solving patterns for complex systems.<\/p>\n<p><\/p>\n<p>Framework choice has no absolute right or wrong \u2014 only suitability. Next.js suits rapid development and most scenarios, but if you want complete control, deep learning, or special requirements, building from scratch is also a valid choice.<\/p>\n<p><\/p>\n<p><strong>Links<\/strong>: <a href=\"https:\/\/nsbp.erishen.cn\/\">Live Demo<\/a> | <a href=\"https:\/\/github.com\/erishen\/nsbp\">GitHub<\/a> | CLI: <code>npx nsbp-cli create my-app<\/code><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction In the React ecosystem, Next.js stands as  [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"english_url":"","chinese_url":"https:\/\/erishen.cn\/?p=222","footnotes":""},"class_list":["post-238","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/pages\/238","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/erishen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=238"}],"version-history":[{"count":3,"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/pages\/238\/revisions"}],"predecessor-version":[{"id":254,"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/pages\/238\/revisions\/254"}],"wp:attachment":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=238"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}