--- title: "The home for Sarthak's work." url: "https://sarthakmishra.com/" description: "Sarthak Mishra designs and builds products, writes about design, code, startups and AI, and helps teams with design systems and website migrations." image: "https://sarthakmishra.com/sarthak-og-image.webp" --- [View HTML](https://sarthakmishra.com/) · [Site index](https://sarthakmishra.com/llms.txt) Tap or press Enter to change its expression. You can also hold or drag it. The home for\ Sarthak's work. =============== I design and build products. Since 2018 that's meant bootstrapping my own ventures in design, AI and growth, and writing here about what works and what doesn't. [Learn more about me](https://sarthakmishra.com/about) If you need a hand, I build brand-specific design systems and migrate sites from no-code builders to code you own. [See services](https://sarthakmishra.com/services) [Let's work together](https://sarthakmishra.com/contact)[Sarthak's GitHub](https://github.com/SarthakMishra)[Sarthak's LinkedIn](https://www.linkedin.com/in/itssarthakmishra/)[Sarthak's X/Twitter](https://x.com/MishraMishry)[Sarthak's RSS Feed](https://sarthakmishra.com/rss.xml) ## Ventures. [![Winitiate logo](data:image/svg+xml,%3csvg%20width='80'%20height='80'%20viewBox='0%200%2080%2080'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3crect%20width='80'%20height='80'%20fill='%233B82F6'/%3e%3crect%20x='14.8403'%20y='28.5527'%20width='9.44693'%20height='29.6664'%20rx='4.72347'%20transform='rotate\(-25%2014.8403%2028.5527\)'%20fill='white'/%3e%3crect%20x='29.4502'%20y='28.5527'%20width='9.44693'%20height='29.6664'%20rx='4.72347'%20transform='rotate\(-25%2029.4502%2028.5527\)'%20fill='white'/%3e%3crect%20x='44.0601'%20y='28.5527'%20width='9.44693'%20height='29.6664'%20rx='4.72347'%20transform='rotate\(-25%2044.0601%2028.5527\)'%20fill='white'/%3e%3c/svg%3e)](https://sarthakmishra.com/work/winitiate) ### [Winitiate](https://sarthakmishra.com/work/winitiate) [2025](https://sarthakmishra.com/work/winitiate) [![Codemap logo](data:image/svg+xml,%3csvg%20width='80'%20height='80'%20viewBox='0%200%2080%2080'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3crect%20width='80'%20height='80'%20fill='%23FF5A51'/%3e%3cpath%20d='M40.0007%2049.8457C43.3401%2049.8458%2046.0474%2052.5532%2046.0476%2055.8926C46.0476%2059.2321%2043.3402%2061.9393%2040.0007%2061.9395C36.6611%2061.9395%2033.9539%2059.2322%2033.9539%2055.8926C33.954%2052.5531%2036.6612%2049.8457%2040.0007%2049.8457ZM22.0632%2038.4961C25.131%2038.4963%2027.6179%2040.9839%2027.6179%2044.0518C27.6177%2047.1194%2025.1309%2049.6062%2022.0632%2049.6064C18.9954%2049.6064%2016.5078%2047.1195%2016.5076%2044.0518C16.5076%2040.9838%2018.9953%2038.4961%2022.0632%2038.4961ZM57.9373%2038.4961C61.0052%2038.4961%2063.4919%2040.9838%2063.4919%2044.0518C63.4917%2047.1195%2061.0051%2049.6064%2057.9373%2049.6064C54.8694%2049.6064%2052.3828%2047.1195%2052.3826%2044.0518C52.3826%2040.9838%2054.8693%2038.4961%2057.9373%2038.4961ZM39.9998%2032.8691C40.7978%2032.8691%2041.4451%2033.5164%2041.4451%2034.3145V47.3125C41.4448%2048.1104%2040.7977%2048.7568%2039.9998%2048.7568C39.202%2048.7567%2038.5557%2048.1103%2038.5554%2047.3125V34.3145C38.5554%2033.5165%2039.2018%2032.8693%2039.9998%2032.8691ZM32.4128%2030.5C32.9235%2029.9391%2033.7922%2029.8978%2034.3533%2030.4082C34.9142%2030.9188%2034.9555%2031.7876%2034.4451%2032.3486L28.6375%2038.7285C28.1267%2039.2893%2027.258%2039.33%2026.697%2038.8193C26.136%2038.3086%2026.0955%2037.4399%2026.6062%2036.8789L32.4128%2030.5ZM45.6462%2030.4082C46.2073%2029.8977%2047.076%2029.939%2047.5867%2030.5L53.3933%2036.8789C53.904%2037.4399%2053.8635%2038.3086%2053.3025%2038.8193C52.7415%2039.3299%2051.8728%2039.2894%2051.3621%2038.7285L45.5544%2032.3486C45.0441%2031.7876%2045.0853%2030.9188%2045.6462%2030.4082ZM39.9998%2018.0566C43.7242%2018.0566%2046.7439%2021.0763%2046.7439%2024.8008C46.7438%2028.5251%2043.7241%2031.5439%2039.9998%2031.5439C36.2755%2031.5438%2033.2567%2028.525%2033.2566%2024.8008C33.2566%2021.0764%2036.2754%2018.0568%2039.9998%2018.0566Z'%20fill='white'/%3e%3c/svg%3e)](https://sarthakmishra.com/work/codemap) ### [Codemap](https://sarthakmishra.com/work/codemap) [2025](https://sarthakmishra.com/work/codemap) [![Learnway logo](data:image/svg+xml,%3csvg%20width='80'%20height='80'%20viewBox='0%200%2080%2080'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3crect%20width='80'%20height='80'%20fill='%2300BC7D'/%3e%3cpath%20d='M49.1471%2055.0978C50.1987%2054.6834%2051.2179%2054.191%2052.196%2053.6247V61.3405C52.196%2061.7448%2052.0354%2062.1326%2051.7495%2062.4185C51.4636%2062.7044%2051.0759%2062.865%2050.6716%2062.865C50.2672%2062.865%2049.8795%2062.7044%2049.5936%2062.4185C49.3077%2062.1326%2049.1471%2061.7448%2049.1471%2061.3405V55.0978ZM51.3881%2038.2465L40.7167%2032.5544C40.3606%2032.3726%2039.9474%2032.3379%2039.566%2032.4578C39.1846%2032.5777%2038.8656%2032.8426%2038.6777%2033.1955C38.4897%2033.5483%2038.4479%2033.9609%2038.5613%2034.3443C38.6746%2034.7277%2038.9341%2035.0512%2039.2837%2035.2452L48.1943%2039.9977L51.4338%2038.2713L51.3881%2038.2465ZM63.5839%2032.5544L40.7167%2020.3586C40.4961%2020.2411%2040.2501%2020.1797%2040.0002%2020.1797C39.7503%2020.1797%2039.5042%2020.2411%2039.2837%2020.3586L16.4164%2032.5544C16.1725%2032.6844%2015.9685%2032.8783%2015.8263%2033.1153C15.684%2033.3522%2015.6089%2033.6234%2015.6089%2033.8998C15.6089%2034.1762%2015.684%2034.4474%2015.8263%2034.6843C15.9685%2034.9213%2016.1725%2035.1152%2016.4164%2035.2452L21.7063%2038.0674V47.2943C21.7048%2048.0431%2021.9804%2048.766%2022.48%2049.3238C24.9764%2052.104%2030.5693%2056.7671%2040.0002%2056.7671C43.1272%2056.7929%2046.2308%2056.2265%2049.1471%2055.0978V40.5065L48.1943%2039.9977L40.0002%2044.3673L23.9607%2035.8054L20.3724%2033.8998L40.0002%2023.4323L59.6279%2033.8998L56.0492%2035.8054H56.0377L51.4338%2038.2713C51.6656%2038.4051%2051.8581%2038.5976%2051.9919%2038.8294C52.1257%2039.0612%2052.1961%2039.3242%2052.196%2039.5918V53.6247C54.184%2052.4771%2055.9804%2051.026%2057.5203%2049.3238C58.02%2048.766%2058.2956%2048.0431%2058.294%2047.2943V38.0674L63.5839%2035.2452C63.8279%2035.1152%2064.0318%2034.9213%2064.1741%2034.6843C64.3163%2034.4474%2064.3914%2034.1762%2064.3914%2033.8998C64.3914%2033.6234%2064.3163%2033.3522%2064.1741%2033.1153C64.0318%2032.8783%2063.8279%2032.6844%2063.5839%2032.5544Z'%20fill='white'/%3e%3c/svg%3e)](https://sarthakmishra.com/work/learnway) ### [Learnway](https://sarthakmishra.com/work/learnway) [2024](https://sarthakmishra.com/work/learnway) [![Drool logo](https://sarthakmishra.com/assets/drool-logo-Cutmt6_x.webp)](https://sarthakmishra.com/work/drool) ### [Drool](https://sarthakmishra.com/work/drool) [2023](https://sarthakmishra.com/work/drool) [![Altpack logo](data:image/svg+xml,%3csvg%20width='80'%20height='80'%20viewBox='0%200%2080%2080'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3crect%20width='80'%20height='80'%20fill='%232D2D2D'/%3e%3cg%20clip-path='url\(%23clip0_37_229\)'%3e%3cpath%20d='M48.58%2047.0313C55.7999%2046.2861%2061.4154%2040.1394%2061.3191%2032.702C61.2229%2024.9865%2054.6127%2018.6758%2046.8972%2018.6758H34.5716C33.6446%2018.6758%2032.8923%2019.4281%2032.8923%2020.3551V31.5468C32.8923%2032.2777%2033.4521%2032.8803%2034.1794%2032.948C40.9751%2033.5577%2046.3909%2038.9521%2047.0398%2045.7335C47.1147%2046.5143%2047.7956%2047.1097%2048.58%2047.0277V47.0313Z'%20fill='white'/%3e%3cpath%20d='M45.6886%2047.1072H34.5931C33.6554%2047.1072%2032.8924%2046.3442%2032.8924%2045.4065V34.3859C32.8924%2033.5373%2032.1544%2032.8848%2031.3094%2032.9775C24.2036%2033.7655%2018.6772%2039.791%2018.6772%2047.1072C18.6772%2054.9581%2025.0415%2061.3223%2032.8924%2061.3223C33.1527%2061.3223%2033.4094%2061.3152%2033.6661%2061.301H45.4283C46.3553%2061.301%2047.1076%2060.5487%2047.1076%2059.6217V48.5262C47.1076%2047.7418%2046.473%2047.1072%2045.6886%2047.1072V47.1072Z'%20fill='white'/%3e%3c/g%3e%3cdefs%3e%3cclipPath%20id='clip0_37_229'%3e%3crect%20width='42.6456'%20height='42.6456'%20fill='white'%20transform='translate\(18.6772%2018.6758\)'/%3e%3c/clipPath%3e%3c/defs%3e%3c/svg%3e)](https://sarthakmishra.com/work/altpack) ### [Altpack](https://sarthakmishra.com/work/altpack) [2022](https://sarthakmishra.com/work/altpack) [![Weaveu logo](https://sarthakmishra.com/assets/weaveu-logo-DVczM4yJ.webp)](https://sarthakmishra.com/work/weaveu) ### [Weaveu](https://sarthakmishra.com/work/weaveu) [2021](https://sarthakmishra.com/work/weaveu) ## Services I offer. [View all services](https://sarthakmishra.com/services) [![Coral hands arranging paper wireframes and desktop and mobile landing-page designs on a navy desk.](https://sarthakmishra.com/assets/saas-landing-pages-hero-DdPZA-Vr.jpeg)](https://sarthakmishra.com/services/saas-landing-pages) [Starting from $750](https://sarthakmishra.com/services/saas-landing-pages) ## [SaaS landing page design and build](https://sarthakmishra.com/services/saas-landing-pages) [One SaaS landing page, from the first wireframe to launch. I handle the design and build for a fixed price.](https://sarthakmishra.com/services/saas-landing-pages) [![Coral, lavender and amber interface tiles connecting along curved node paths into a structured browser window with a bar chart, toggle and cursor on a cobalt-blue grid.](https://sarthakmishra.com/assets/ui-sprint-hero-Dg4NUAcN.jpeg)](https://sarthakmishra.com/services/ui-sprint) [Starting from $500](https://sarthakmishra.com/services/ui-sprint) ## [Product UI Sprints for SaaS workflows and React apps](https://sarthakmishra.com/services/ui-sprint) [A focused sprint to redesign or implement one SaaS workflow in Figma, React, or both, using the components your product already has.](https://sarthakmishra.com/services/ui-sprint) [![A hand pressing a purple button on a mint interface panel with three golden-orange cards, on a peach background.](https://sarthakmishra.com/assets/design-systems-hero-CKM-ToLo.jpeg)](https://sarthakmishra.com/services/design-systems) [Starting from $1,000](https://sarthakmishra.com/services/design-systems) ## [Design Systems built for your brand and your AI tools](https://sarthakmishra.com/services/design-systems) [A brand-specific React and shadcn/ui library with tokens, custom components, lint rules and agent tooling, so every new screen looks like it belongs.](https://sarthakmishra.com/services/design-systems) [![An illustrated laptop beside a coffee mug with curling steam, a notebook, pen, and sticky note on a citron background.](https://sarthakmishra.com/assets/website-migrations-hero-_Oh-v7h0.jpeg)](https://sarthakmishra.com/services/migrations) [Starting from $100](https://sarthakmishra.com/services/migrations) ## [Website Migrations from Framer, Webflow, and more](https://sarthakmishra.com/services/migrations) [A faithful rebuild of your existing marketing site in Astro, Next.js or TanStack Start, with CMS integration included.](https://sarthakmishra.com/services/migrations) ## Featured work. [View all work](https://sarthakmishra.com/work) [![Illustrated desk calendar showing a sunny day, with curling ribbon, pencil, and paperclip.](https://sarthakmishra.com/assets/mynextfiling-hero-CLY_LXjk.jpeg)](https://sarthakmishra.com/work/my-next-filing) [Sep 11 2026](https://sarthakmishra.com/work/my-next-filing) ## [My Next Filing - A Tax Plan for Freelancers in India](https://sarthakmishra.com/work/my-next-filing) [A browser-only tax planner for solo freelancers in India. Answer a few questions and get your next action, its due date, and what you still owe.](https://sarthakmishra.com/work/my-next-filing) [![Browser screenshot of Engage360 activity dashboard with revenue charts, invoice metrics, and a new leads table.](https://sarthakmishra.com/assets/engage360-hero-C0AM-5G8.jpeg)](https://sarthakmishra.com/work/ux-folio) [Nov 15 2024](https://sarthakmishra.com/work/ux-folio) ## [UX-Folio - Design Explorations & Experiments](https://sarthakmishra.com/work/ux-folio) [A collection of interesting design work, hackathon projects, and experimental interfaces created during my time at Drool agency.](https://sarthakmishra.com/work/ux-folio) [![Angled collage of grayscale app onboarding screens with lime-green buttons, profile photos, payment forms, and filters.](https://sarthakmishra.com/assets/mydrip-hero-BW8DGAja.jpeg)](https://sarthakmishra.com/work/mydrip) [Sep 15 2024](https://sarthakmishra.com/work/mydrip) ## [My Drip - AI-Powered Personalized Shopping Experience](https://sarthakmishra.com/work/mydrip) [An innovative MVP that combines generative AI with personalized eCommerce, delivering daily outfit recommendations with realistic photos of users wearing them.](https://sarthakmishra.com/work/mydrip) [![Desktop and mobile website mockups for Phidata, promoting AI app building with blue gradient waves.](https://sarthakmishra.com/assets/phidata-hero-hYuqYO84.jpeg)](https://sarthakmishra.com/work/phidata) [Apr 20 2024](https://sarthakmishra.com/work/phidata) ## [Phidata - AI Assistant Platform Landing Page](https://sarthakmishra.com/work/phidata) [A comprehensive landing page redesign for an innovative AI assistant platform, focusing on clear communication and conversion optimization.](https://sarthakmishra.com/work/phidata) ## Recent posts. [View all posts](https://sarthakmishra.com/blog) [![Hands guide three sequential interface cards from image selection to a completed checkmark.](https://sarthakmishra.com/assets/how-startups-designed-their-mvps-hero-BqVRm4If.jpeg)](https://sarthakmishra.com/blog/how-startups-designed-their-mvps) [Nov 03 2026](https://sarthakmishra.com/blog/how-startups-designed-their-mvps) ## [How Dropbox, DoorDash, Airbnb and Stripe designed their MVPs](https://sarthakmishra.com/blog/how-startups-designed-their-mvps) [Early UI and founder stories from Dropbox, DoorDash, Airbnb, Stripe, Zapier, and Product Hunt. What to design first, reuse, and leave for later.](https://sarthakmishra.com/blog/how-startups-designed-their-mvps) [![Illustrated hands frame a floating video player with play button against an abstract purple background.](https://sarthakmishra.com/assets/adaptive-video-streaming-hero-BdJfL03g.jpeg)](https://sarthakmishra.com/blog/adaptive-video-streaming) [Nov 01 2026](https://sarthakmishra.com/blog/adaptive-video-streaming) ## [Adaptive video with FFmpeg, Vite and a custom player](https://sarthakmishra.com/blog/adaptive-video-streaming) [How I turn recordings into cached AV1 and H.264 streams with FFmpeg and Vite, then play them with native HLS or Mattebox behind custom React controls.](https://sarthakmishra.com/blog/adaptive-video-streaming) [![Gold coins and a clock spiral into a deep blue vortex.](https://sarthakmishra.com/assets/scoping-a-saas-project-hero-CJ2n9BSM.jpeg)](https://sarthakmishra.com/blog/scoping-a-saas-project) [Oct 31 2026](https://sarthakmishra.com/blog/scoping-a-saas-project) ## [How to scope a SaaS project you can afford to finish](https://sarthakmishra.com/blog/scoping-a-saas-project) [How I approach SaaS scope and pricing as a former agency owner and current freelancer, and what to ask when budgeting for design and development.](https://sarthakmishra.com/blog/scoping-a-saas-project) [![Colorful illustration of a central palm tree icon surrounded by charts, interface controls, and hands.](https://sarthakmishra.com/assets/optimizing-tanstack-start-hero-CzYctZnB.jpeg)](https://sarthakmishra.com/blog/optimizing-tanstack-start) [Oct 26 2026](https://sarthakmishra.com/blog/optimizing-tanstack-start) ## [Optimizing the sh\*t out of TanStack Start](https://sarthakmishra.com/blog/optimizing-tanstack-start) [A practical TanStack Start performance guide: how cutting downloads and fixing fonts and images took my home page from 73 to 100 on mobile Lighthouse.](https://sarthakmishra.com/blog/optimizing-tanstack-start) ## Subscribe to my newsletter You'll get two emails initially: a confirmation link to verify your subscription, followed by a welcome message. Thanks for joining! You can read all of my [previous issues here](https://sarthakmishra.com/newsletter) ## Structured data ```json { "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": "https://sarthakmishra.com/#website", "url": "https://sarthakmishra.com/", "name": "Sarthak Mishra", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "inLanguage": "en" }, { "@type": "Person", "@id": "https://sarthakmishra.com/#person", "name": "Sarthak Mishra", "url": "https://sarthakmishra.com/about", "jobTitle": "Design Engineer", "image": "https://sarthakmishra.com/og-images/sarthak-mishra.webp", "sameAs": [ "https://github.com/SarthakMishra", "https://www.linkedin.com/in/itssarthakmishra/", "https://x.com/MishraMishry" ] }, { "@type": "WebPage", "@id": "https://sarthakmishra.com/#webpage", "url": "https://sarthakmishra.com/", "name": "Sarthak Mishra - Designer who codes!", "description": "Sarthak Mishra designs and builds products, writes about design, code, startups and AI, and helps teams with design systems and website migrations.", "inLanguage": "en", "isPartOf": { "@id": "https://sarthakmishra.com/#website" }, "about": { "@id": "https://sarthakmishra.com/#person" } } ] } ``` --- title: "About me." url: "https://sarthakmishra.com/about" description: "Sarthak Mishra designs and builds products, writes about design, code, startups and AI, and helps teams with design systems and website migrations." image: "https://sarthakmishra.com/sarthak-og-image.webp" --- [View HTML](https://sarthakmishra.com/about) · [Site index](https://sarthakmishra.com/llms.txt) # About me. Design Engineer. Freelancing in design and frontend development. ![Sarthak Mishra](https://sarthakmishra.com/assets/sarthak-photo-B-ntsY2b.jpeg)Open a larger image My journey into the world of design and entrepreneurship began in an unexpected way. At 19, I started a T-shirt business that opened my eyes to the fascinating world of building something from scratch. That early taste of entrepreneurship was intoxicating—the thrill of creating, the challenge of selling, and the satisfaction of seeing an idea come to life. Then came 2020, and with it, one of the most pivotal decisions of my life. I was in my 4th semester at NID (National Institute of Design) when COVID hit. While the world felt uncertain, I saw an opportunity to carve my own path. I made the bold decision to drop out and dive headfirst into learning UX design and building a career around it. Looking back, that decision was both terrifying and liberating. Without the safety net of a traditional education path, I had to learn everything the hard way—through experimentation, failure, and relentless curiosity. Since then, I've worn many hats and embraced every opportunity to grow. I've interned at scrappy startups where I learned the art of doing more with less, founded [my own company](https://sarthakmishra.com/work/weaveu) and experienced the rollercoaster of entrepreneurship firsthand, ran a [design agency](https://sarthakmishra.com/work/drool) where I honed my craft while helping others bring their visions life, and taught myself to code because I believe the best designers understand the medium they're designing for. I even took a shot at [YCombinator](https://sarthakmishra.com/work/learnway)—because why not aim high? While not every venture has been a traditional "success," each experience has been a masterclass in resilience, adaptation, and growth. Today, I freelance with [early-stage startups](https://sarthakmishra.com/work/sellscale) as a designer, and I've extended my [services](https://sarthakmishra.com/services) to frontend development: design systems built for a team's brand, and site migrations off no-code builders onto code they own. I'm still most excited by the intersection of design, AI, and growth. In September 2026, [My Next Filing](https://sarthakmishra.com/work/my-next-filing), a tax planner I built for freelancers in India, made the final of [Build What Moves India](https://www.youtube.com/watch?v=okJqVvkQTQ8), Varun Mayya's competition run with the OpenAI team. Out of 13,694 applications, it was one of 10 projects pitched on stage. I didn't place, but I came home with blunt, useful feedback. Through all these experiences, I've learned that the most valuable skill isn't any particular tool or technique—it's the ability to adapt to surprises and grow from every challenge. In a world that's constantly changing, being comfortable with uncertainty and maintaining a growth mindset has been my secret weapon. A short personal note: I recently ticked off a bucket-list item, a six-day, 1,400 km bike trip from Guwahati to Tawang. When I'm not designing or coding, you'll find me exploring new ideas, reading about emerging technologies, or working on my [next bootstrapped venture](https://sarthakmishra.com/work/winitiate). I believe the best is yet to come, and I'm excited to see where this journey takes me next. ## Structured data ```json { "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": "https://sarthakmishra.com/#website", "url": "https://sarthakmishra.com/", "name": "Sarthak Mishra", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "inLanguage": "en" }, { "@type": "Person", "@id": "https://sarthakmishra.com/#person", "name": "Sarthak Mishra", "url": "https://sarthakmishra.com/about", "jobTitle": "Design Engineer", "image": "https://sarthakmishra.com/og-images/sarthak-mishra.webp", "sameAs": [ "https://github.com/SarthakMishra", "https://www.linkedin.com/in/itssarthakmishra/", "https://x.com/MishraMishry" ] }, { "@type": "ProfilePage", "@id": "https://sarthakmishra.com/about#webpage", "url": "https://sarthakmishra.com/about", "name": "Sarthak Mishra - Designer who codes!", "description": "Sarthak Mishra designs and builds products, writes about design, code, startups and AI, and helps teams with design systems and website migrations.", "inLanguage": "en", "isPartOf": { "@id": "https://sarthakmishra.com/#website" }, "about": { "@id": "https://sarthakmishra.com/#person" }, "mainEntity": { "@id": "https://sarthakmishra.com/#person" } } ] } ``` --- title: "Newsletter archive." url: "https://sarthakmishra.com/newsletter" description: "Archive of all newsletter issues from Sarthak's Weekly Digest. Design, code, startups and technology insights delivered weekly." image: "https://sarthakmishra.com/sarthak-og-image.webp" --- [View HTML](https://sarthakmishra.com/newsletter) · [Site index](https://sarthakmishra.com/llms.txt) # Newsletter archive. Weekly insights about design, code, startups and technology. ## [Rethinking my agency website#001](https://sarthakmishra.com/newsletter/issue-1) [Why I'm ditching the traditional agency website playbook and building a resource site targeting founders in my ICP, using programmatic SEO.](https://sarthakmishra.com/newsletter/issue-1) ## Subscribe to my newsletter You'll get two emails initially: a confirmation link to verify your subscription, followed by a welcome message. Thanks for joining! You can read all of my [previous issues here](https://sarthakmishra.com/newsletter) ## Structured data ```json { "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": "https://sarthakmishra.com/#website", "url": "https://sarthakmishra.com/", "name": "Sarthak Mishra", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "inLanguage": "en" }, { "@type": "Person", "@id": "https://sarthakmishra.com/#person", "name": "Sarthak Mishra", "url": "https://sarthakmishra.com/about", "jobTitle": "Design Engineer", "image": "https://sarthakmishra.com/og-images/sarthak-mishra.webp", "sameAs": [ "https://github.com/SarthakMishra", "https://www.linkedin.com/in/itssarthakmishra/", "https://x.com/MishraMishry" ] }, { "@type": "CollectionPage", "@id": "https://sarthakmishra.com/newsletter#webpage", "url": "https://sarthakmishra.com/newsletter", "name": "Newsletter archive - Sarthak Mishra", "description": "Archive of all newsletter issues from Sarthak's Weekly Digest. Design, code, startups and technology insights delivered weekly.", "inLanguage": "en", "isPartOf": { "@id": "https://sarthakmishra.com/#website" }, "mainEntity": { "@id": "https://sarthakmishra.com/newsletter#items" } }, { "@type": "ItemList", "@id": "https://sarthakmishra.com/newsletter#items", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Rethinking my agency website", "url": "https://sarthakmishra.com/newsletter/issue-1" } ] } ] } ``` --- title: "Services I offer." url: "https://sarthakmishra.com/services" description: "Design and development services: website migrations from Framer, Webflow and Wix, and design systems built for your brand and your AI tools." image: "https://sarthakmishra.com/sarthak-og-image.webp" --- [View HTML](https://sarthakmishra.com/services) · [Site index](https://sarthakmishra.com/llms.txt) # Services I offer. Looking for something else? [Tell me](https://sarthakmishra.com/contact) about your project. [![Coral hands arranging paper wireframes and desktop and mobile landing-page designs on a navy desk.](https://sarthakmishra.com/assets/saas-landing-pages-hero-DdPZA-Vr.jpeg)](https://sarthakmishra.com/services/saas-landing-pages) [Starting from $750](https://sarthakmishra.com/services/saas-landing-pages) ## [SaaS landing page design and build](https://sarthakmishra.com/services/saas-landing-pages) [One SaaS landing page, from the first wireframe to launch. I handle the design and build for a fixed price.](https://sarthakmishra.com/services/saas-landing-pages) [![Coral, lavender and amber interface tiles connecting along curved node paths into a structured browser window with a bar chart, toggle and cursor on a cobalt-blue grid.](https://sarthakmishra.com/assets/ui-sprint-hero-Dg4NUAcN.jpeg)](https://sarthakmishra.com/services/ui-sprint) [Starting from $500](https://sarthakmishra.com/services/ui-sprint) ## [Product UI Sprints for SaaS workflows and React apps](https://sarthakmishra.com/services/ui-sprint) [A focused sprint to redesign or implement one SaaS workflow in Figma, React, or both, using the components your product already has.](https://sarthakmishra.com/services/ui-sprint) [![A hand pressing a purple button on a mint interface panel with three golden-orange cards, on a peach background.](https://sarthakmishra.com/assets/design-systems-hero-CKM-ToLo.jpeg)](https://sarthakmishra.com/services/design-systems) [Starting from $1,000](https://sarthakmishra.com/services/design-systems) ## [Design Systems built for your brand and your AI tools](https://sarthakmishra.com/services/design-systems) [A brand-specific React and shadcn/ui library with tokens, custom components, lint rules and agent tooling, so every new screen looks like it belongs.](https://sarthakmishra.com/services/design-systems) [![An illustrated laptop beside a coffee mug with curling steam, a notebook, pen, and sticky note on a citron background.](https://sarthakmishra.com/assets/website-migrations-hero-_Oh-v7h0.jpeg)](https://sarthakmishra.com/services/migrations) [Starting from $100](https://sarthakmishra.com/services/migrations) ## [Website Migrations from Framer, Webflow, and more](https://sarthakmishra.com/services/migrations) [A faithful rebuild of your existing marketing site in Astro, Next.js or TanStack Start, with CMS integration included.](https://sarthakmishra.com/services/migrations) ## Structured data ```json { "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": "https://sarthakmishra.com/#website", "url": "https://sarthakmishra.com/", "name": "Sarthak Mishra", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "inLanguage": "en" }, { "@type": "Person", "@id": "https://sarthakmishra.com/#person", "name": "Sarthak Mishra", "url": "https://sarthakmishra.com/about", "jobTitle": "Design Engineer", "image": "https://sarthakmishra.com/og-images/sarthak-mishra.webp", "sameAs": [ "https://github.com/SarthakMishra", "https://www.linkedin.com/in/itssarthakmishra/", "https://x.com/MishraMishry" ] }, { "@type": "CollectionPage", "@id": "https://sarthakmishra.com/services#webpage", "url": "https://sarthakmishra.com/services", "name": "Services I offer - Sarthak Mishra", "description": "Design and development services: website migrations from Framer, Webflow and Wix, and design systems built for your brand and your AI tools.", "inLanguage": "en", "isPartOf": { "@id": "https://sarthakmishra.com/#website" }, "mainEntity": { "@id": "https://sarthakmishra.com/services#items" } }, { "@type": "ItemList", "@id": "https://sarthakmishra.com/services#items", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "SaaS landing page design and build", "url": "https://sarthakmishra.com/services/saas-landing-pages" }, { "@type": "ListItem", "position": 2, "name": "Product UI Sprints for SaaS workflows and React apps", "url": "https://sarthakmishra.com/services/ui-sprint" }, { "@type": "ListItem", "position": 3, "name": "Design Systems built for your brand and your AI tools", "url": "https://sarthakmishra.com/services/design-systems" }, { "@type": "ListItem", "position": 4, "name": "Website Migrations from Framer, Webflow, and more", "url": "https://sarthakmishra.com/services/migrations" } ] } ] } ``` --- title: "How Dropbox, DoorDash, Airbnb and Stripe designed their MVPs" url: "https://sarthakmishra.com/blog/how-startups-designed-their-mvps" description: "Early UI and founder stories from Dropbox, DoorDash, Airbnb, Stripe, Zapier, and Product Hunt. What to design first, reuse, and leave for later." image: "https://sarthakmishra.com/og-images/blog/how-startups-designed-their-mvps.webp" --- [View HTML](https://sarthakmishra.com/blog/how-startups-designed-their-mvps) · [Site index](https://sarthakmishra.com/llms.txt) November 3, 2026 # How Dropbox, DoorDash, Airbnb and Stripe designed their MVPs Early UI and founder stories from Dropbox, DoorDash, Airbnb, Stripe, Zapier, and Product Hunt. What to design first, reuse, and leave for later. [Let's work together](https://sarthakmishra.com/blog/how-startups-designed-their-mvps#inquiry) ![Hands guide three sequential interface cards from image selection to a completed checkmark.](https://sarthakmishra.com/assets/how-startups-designed-their-mvps-hero-B8idXjsm.jpeg) You want to get your MVP in front of people quickly. You also want to give them a fair chance to use it. If they can’t understand what it does or how to use it, it’s hard to learn whether they’d find it useful. Start with the first thing you want someone to accomplish. Work through what they need to see and do, then decide what can wait, what an existing tool can cover, and what your team can handle by hand. Dropbox put its effort into one behavior people had to see working. DoorDash handled its first orders by hand, and Airbnb’s founders took listing photos themselves. Stripe started with a friend who could put up with the gaps, and Product Hunt began inside someone else’s tool. Their founders’ accounts show what each designed first and what it left for later. ## Dropbox’s demo was a folder that kept itself up to date In April 2007, Drew Houston [posted an early Dropbox demo on Hacker News](https://news.ycombinator.com/item?id=8863). In the discussion, he explained that files lived in a local folder and synced in the background. You could work offline and have changes sync when you reconnected. The important interaction happened in a folder people already knew how to use. Saving a file was enough to keep every copy up to date. Houston later explained in a [Mixergy interview](https://mixergy.com/interviews/drew-houston-dropbox-interview/) that the prototype could demonstrate the idea before the software was ready for unrestricted use. Desktop reliability was the harder problem. He also said the first Hacker News video was recorded in one take and wasn’t doctored. The software existed. The screencast let viewers react to the interaction without installing Dropbox. The closed beta let a limited group install it and sync their own files, so Houston could get feedback from actual use. ![Dropbox's early web interface in Firefox on Windows XP, showing recent file changes and links to files, workspaces, and account settings.](https://sarthakmishra.com/assets/dropbox-demo-2007-CpJ97Lzy.jpeg) A frame from Drew Houston’s [original April 2007 Dropbox screencast.](https://s3.amazonaws.com/scast/video.flv) That launch thread also produced BrandonM’s [skeptical comment](https://news.ycombinator.com/item?id=9224). He suggested you could get the same thing with an FTP account, curlftpfs, and SVN or CVS. Just a little systems administration between you and your files. ![BrandonM's April 5, 2007 Hacker News comment questioning Dropbox's advantages over FTP and version-control tools, offline access, and revenue prospects.](https://sarthakmishra.com/assets/dropbox-hacker-news-2007-CbjtDypX.jpeg) BrandonM’s April 5, 2007 comment in the [original Hacker News thread](https://news.ycombinator.com/item?id=8863). Houston [replied](https://news.ycombinator.com/item?id=9272) that “the user experience (on windows at least) with online drives generally sucks.” A local folder meant nobody waited on a remote drive. I don’t buy “you can already do that” as a reason to dismiss a product. **With Dropbox, a familiar folder did work that otherwise meant configuring and combining several tools.** You could use Dropbox without having to learn any of those tools. ## DoorDash’s MVP was a phone number, a car, and a few PDFs In his [2014 startup lecture](https://www.ycombinator.com/library/JL-how-to-start-a-startup-getting-started-getting-press-and-doing-things-that-don-t-scale), DoorDash cofounder Stanley Tang described putting PDF restaurant menus and a phone number on a landing page. The team wanted to find out whether people would call to order delivery. They did. The founders picked up the food and delivered it themselves. Existing tools covered other needs, including Square for charging customers and Google Docs for tracking orders. Driving, dispatching, and answering support questions taught them about the work they would later automate. **DoorDash could take real orders before it had a custom dispatch system.** The founders’ own driving and dispatching stood in for it until they knew people would order. If your team can take a request and do the work by hand, use existing tools to manage it. A custom dashboard can wait. The customer still needs to know what they’re being promised, and someone on the team has to own delivering it. ![DoorDash's 2013 website offering Palo Alto restaurant delivery for six dollars, with restaurant selection and delivery hours.](https://sarthakmishra.com/assets/doordash-launch-2013-kqHweE4W.jpeg) DoorDash’s June 2013 launch website, after the first phone-order experiment. Screenshot shared in [cofounder Tony Xu’s anniversary note](https://about.doordash.com/en-us/news/doordash-celebrates-its-10th-dashiversary). Tang also recalled a limit to this approach in a [later Stanford talk](https://stvp.stanford.edu/av/stanley-tang-doordash-scaling-unscalable-entire-talk). In September 2013, months after the first delivery, a football-night surge overwhelmed the team. They had too few drivers and hadn’t built a way to stop incoming website orders. Deliveries ran late. A way to pause orders belongs in the MVP. Agree on how much work the team can take and how it will stop new requests before the team gets overwhelmed. Hungry customers still expect dinner (even if the founders are doing the driving). ![Tony Xu walking along a Palo Alto sidewalk carrying a red insulated delivery bag and a paper food bag.](https://sarthakmishra.com/assets/tony-xu-delivering-2014-DbqXaaRp.jpeg) DoorDash cofounder Tony Xu on a delivery in 2014. Photograph by Vicki Thompson / Silicon Valley Business Journal, from [its early DoorDash coverage](https://www.bizjournals.com/sanjose/news/2014/03/17/doordash-enters-food-delivery-fray-with-much.html). ## Airbnb’s founders took the listing photos themselves Airbnb’s first test was small enough for the founders to host the guests themselves. In his [founders’ letter](https://news.airbnb.com/what-makes-airbnb-airbnb), Brian Chesky recalls how he and Joe Gebbia hosted three people on airbeds in their apartment during a 2007 design conference. ![AirBed and Breakfast's 2008 homepage, showing an event search form, separate sections for hosts and guests, and a featured Web 2.0 Expo event.](https://sarthakmishra.com/assets/airbnb-homepage-2008-KqMqORft.jpeg) The AirBed & Breakfast website in 2008, between the first apartment test and the photography work below. Screenshot from [Airbnb’s official timeline](https://news.airbnb.com/about-us). Helping other people become hosts brought different problems. In his [2014 YC lecture](https://www.ycombinator.com/library/JN-how-to-start-a-startup-building-company-culture-part-i), Chesky described staying with a New York host whose home looked much better than its photos. She couldn’t work out how to transfer photos from her camera to her computer. Chesky photographed the home himself. He and Gebbia then handled other photography requests, renting a camera and coordinating the work with spreadsheets. Management tools came later, after they had experience running the service. **Helping hosts supply useful photos became part of Airbnb’s service.** That content work needed attention early, even while the founders were taking the photos themselves. I’d fix weak listing content before paying for a redesign. Check the content the first customer will see. Does it give them enough information to choose? ## Stripe chose a first user who could work with an unfinished product Stripe began with an API that could charge cards. In a [2012 interview with Mixergy](https://mixergy.com/interviews/patrick-collison-stripe-interview/), cofounder Patrick Collison described the team adding refunds and transaction-viewing features as requests came in. He was unsure whether even a minimal interface for viewing charges existed on day one. The first user was Ross Boucher, a friend who needed to accept payments and whose technical and design judgment they respected. The founders were upfront about the product’s gaps and would build in response to his feedback. Collison said that level of incompleteness would not have been acceptable for an ordinary paying user. In [another early interview](https://www.startupgrind.com/blog/from-the-vault-patrick-collison-stripe-full-startup-grind-interview-2012/), he described manually arranging the underlying account setup through a contact at a payment processor. At that point, they had two users, both friends. By Collison’s [later account at Startup School](https://www.ycrootaccess.com/p/patrick-collison-what-if-you-succeed), the first production use was in January 2010. Public launch followed in September 2011. Before then, Stripe needed security, payment partnerships, and dependable payouts so businesses could sign up and take payments on their own. ![Stripe's circa-2011 homepage showing a short curl request to create a card charge beside the heading An API that gets out of your way.](https://sarthakmishra.com/assets/stripe-homepage-circa-2011-DSiQPoBZ.jpeg) Stripe’s homepage API example, circa 2011. Screenshot reproduced in [Stripe’s API retrospective](https://stripe.dev/blog/payment-api-design), courtesy of the Internet Archive Wayback Machine. Ross could tell the founders directly what was missing, and they could build it for him. **A friend in the room made up for missing setup screens and documentation.** A customer signing up alone would have to rely much more on the product’s setup and instructions. For a developer product, the API and the instructions are part of the interface. Don’t assume a stranger signing up will give you the patience Ross did. Agree on how much personal support the first users will have, then design for what they’ll need to do alone. ## Zapier showed how a tweet could turn into a contact Zapier’s founders were working against a Startup Weekend deadline in 2011. They also had a difficult idea to explain to people who weren’t developers. Connecting two services through their APIs needed to make sense on screen. In [Wade Foster’s account of that Saturday](https://zapier.com/blog/startup-weekend-part-iii-pushing-through-saturday/), Mike Knoop sketched wireframes and gathered feedback from other attendees before building the interface. The frontend and backend were still disconnected that night, but they were already spending time finding out whether people understood the flow. For [Sunday’s demo](https://zapier.com/blog/startup-weekend-part-iv-sunday-demo-day/), **the team dropped the dashboard and concentrated on the interaction the audience would see.** The demonstration connected Twitter mentions to contacts in Highrise, a customer-management tool. Good cut. The audience needed to see what connecting two services would do for them before they needed an overview of their connections. A dashboard appeared as the product developed. Foster’s [2012 year review](https://zapier.com/blog/zapier-100-connected-services-year-review/) describes revising the UI after beta customers asked for something better. ![Zapier's April 2012 dashboard with active, inactive, and unfinished Zaps beside a stream of recent actions.](https://sarthakmishra.com/assets/zapier-dashboard-2012-lYOgen7f.jpeg) Zapier’s Version 1.0 dashboard, April 2012, after the Startup Weekend prototype. Native-resolution frame from [Zapier’s original frontend demo](https://www.youtube.com/watch?v=gswLe6PoAxA). ## Product Hunt started as a Linkydink group Product Hunt began in November 2013 with a Linkydink group. In [his launch announcement](https://www.ryanhoover.me/post/announcing-product-hunt), Ryan Hoover described contributors submitting product links through the group or an email address. Subscribers received a daily digest with credit to each contributor. **A borrowed tool was enough to test the daily habit.** Hoover didn’t need a custom interface to find out whether people kept contributing and reading. ![Product Hunt's archived Linkydink group with November 2013 product links, contributor portraits, and a Subscribe button.](https://sarthakmishra.com/assets/product-hunt-linkydink-2013-BXhCK-ro.jpeg) Product Hunt’s Linkydink group, rendered from the [November 9, 2013 Internet Archive capture](https://web.archive.org/web/20131109095046/http://linkydink.io:80/groups/product-hunt). Linkydink handled link collection and the daily email, but Hoover wanted [a place to discuss those discoveries](https://medium.com/on-startups/the-artifacts-of-product-hunt-11682e9e01dd). Building the custom site meant designing product-submission fields, voting controls, and a way to read and write comments. Cofounder Nathan Bashaw’s [account](https://medium.com/lets-make-things/the-origin-of-product-hunt-7acb09e2593a) describes making names and taglines easy to scan and opening discussions in a side pane. ![Product Hunt's December 2013 website, with product rankings at left and an Assembly discussion pane open at right.](https://sarthakmishra.com/assets/product-hunt-website-2013-esadtEra.jpeg) Product Hunt’s website and discussion pane in December 2013. Original screenshot from [Ryan Hoover’s account of its interaction design](https://www.ryanhoover.me/post/hunting-for-habits-keying-in-on-smart-design-to-make-a-product-irresistible). Hoover and Bashaw [kept Linkydink’s grouping of products by day](https://medium.com/lets-make-things/the-origin-of-product-hunt-7acb09e2593a) in the new site. The email experiment had already given them a structure for presenting each day’s discoveries. ## Put the effort where the first customer needs it I’d use these stories to decide where the design budget goes. | Question | What it changes in the scope | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | What does the first user need to understand or accomplish? | The behavior, content, and interaction that deserve attention first. | | What can a prototype let you observe? | What to test before building a live flow, and what still needs testing with real use. | | Which work can the team handle personally? | Which automation to defer, who owns that work, and the limit on what the team can promise. | | How much help will the first user have? | The setup, instructions, support, and recovery paths needed for that audience. | Familiar controls and a small set of consistent styles can be enough to start. **Spend more time on anything that could leave the customer unsure what to do, what they’re getting, or whether their action worked.** I work on SaaS UI/UX and React frontend development. Use the [inquiry form below](https://sarthakmishra.com/blog/how-startups-designed-their-mvps#inquiry) to share your idea, the problem you’re trying to solve, and any budget or deadline. I can help you choose what to design first and whether to begin with a prototype, a working flow, or a technical investigation. ## Structured data ```json { "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": "https://sarthakmishra.com/#website", "url": "https://sarthakmishra.com/", "name": "Sarthak Mishra", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "inLanguage": "en" }, { "@type": "Person", "@id": "https://sarthakmishra.com/#person", "name": "Sarthak Mishra", "url": "https://sarthakmishra.com/about", "jobTitle": "Design Engineer", "image": "https://sarthakmishra.com/og-images/sarthak-mishra.webp", "sameAs": [ "https://github.com/SarthakMishra", "https://www.linkedin.com/in/itssarthakmishra/", "https://x.com/MishraMishry" ] }, { "@type": "WebPage", "@id": "https://sarthakmishra.com/blog/how-startups-designed-their-mvps#webpage", "url": "https://sarthakmishra.com/blog/how-startups-designed-their-mvps", "name": "How Dropbox, DoorDash, Airbnb and Stripe designed their MVPs", "description": "Early UI and founder stories from Dropbox, DoorDash, Airbnb, Stripe, Zapier, and Product Hunt. What to design first, reuse, and leave for later.", "inLanguage": "en", "isPartOf": { "@id": "https://sarthakmishra.com/#website" }, "mainEntity": { "@id": "https://sarthakmishra.com/blog/how-startups-designed-their-mvps#content" }, "breadcrumb": { "@id": "https://sarthakmishra.com/blog/how-startups-designed-their-mvps#breadcrumb" } }, { "@id": "https://sarthakmishra.com/blog/how-startups-designed-their-mvps#content", "url": "https://sarthakmishra.com/blog/how-startups-designed-their-mvps", "name": "How Dropbox, DoorDash, Airbnb and Stripe designed their MVPs", "description": "Early UI and founder stories from Dropbox, DoorDash, Airbnb, Stripe, Zapier, and Product Hunt. What to design first, reuse, and leave for later.", "mainEntityOfPage": { "@id": "https://sarthakmishra.com/blog/how-startups-designed-their-mvps#webpage" }, "author": { "@id": "https://sarthakmishra.com/#person" }, "datePublished": "2026-11-03T00:00:00.000Z", "inLanguage": "en", "keywords": [ "saas", "mvp", "product-design", "prototyping", "user-experience", "y-combinator" ], "@type": "BlogPosting", "headline": "How Dropbox, DoorDash, Airbnb and Stripe designed their MVPs", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "image": "https://sarthakmishra.com/og-images/blog/how-startups-designed-their-mvps.webp" }, { "@type": "BreadcrumbList", "@id": "https://sarthakmishra.com/blog/how-startups-designed-their-mvps#breadcrumb", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://sarthakmishra.com/" }, { "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://sarthakmishra.com/blog" }, { "@type": "ListItem", "position": 3, "name": "How Dropbox, DoorDash, Airbnb and Stripe designed their MVPs", "item": "https://sarthakmishra.com/blog/how-startups-designed-their-mvps" } ] } ] } ``` --- title: "Adaptive video with FFmpeg, Vite and a custom player" url: "https://sarthakmishra.com/blog/adaptive-video-streaming" description: "How I turn recordings into cached AV1 and H.264 streams with FFmpeg and Vite, then play them with native HLS or Mattebox behind custom React controls." image: "https://sarthakmishra.com/og-images/blog/adaptive-video-streaming.webp" --- [View HTML](https://sarthakmishra.com/blog/adaptive-video-streaming) · [Site index](https://sarthakmishra.com/llms.txt) November 1, 2026 # Adaptive video with FFmpeg, Vite and a custom player How I turn recordings into cached AV1 and H.264 streams with FFmpeg and Vite, then play them with native HLS or Mattebox behind custom React controls. ![Illustrated hands frame a floating video player with play button against an abstract purple background.](https://sarthakmishra.com/assets/adaptive-video-streaming-hero-CnxoDtvI.jpeg) The debug demo video in my [table of contents post](https://sarthakmishra.com/blog/building-perfect-toc-component) is one of 18 videos on this site as of writing this post. I first hosted the TOC demo on Mux in August 2025, then moved it to YouTube that December. I wasn’t using Mux’s analytics, and YouTube was a simpler way to host it without worrying about future costs. Having YouTube’s [branding and related videos](https://developers.google.com/youtube/player_parameters) in the middle of a blog post felt excessive for a clip showing a few seconds of UI. The [My Next Filing](https://sarthakmishra.com/work/my-next-filing) demo was my first self-hosted video. I put an MP4 on the site and used the browser’s controls, which I’d still do for a short clip. For longer recordings, I wanted a sharp version for reading small text and smaller versions for slower connections. Each file also had to fit within Cloudflare’s [25 MiB limit for static assets](https://developers.cloudflare.com/workers/platform/limits/#static-assets). I could have gone back to Mux for the encoding and quality switching. I’d still have uploads and playback IDs to manage separately from the site. I wanted videos to work like images already do here. Put the file in the repo, reference it, and let the build handle the rest. ## Codecs, containers and HLS An MP4 file holds compressed video, usually audio, and timing information. MP4 is the [container format](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Containers). H.264 and AV1 are video codecs, which define how the picture data is compressed and decoded. AAC does the same for audio. Either video codec can go inside an MP4. [HTTP Live Streaming (HLS)](https://www.rfc-editor.org/rfc/rfc8216.html#section-2) uses playlists to tell the player which files to request. A master playlist lists the available versions of a video, each with its own playlist of short segments. With several resolutions available, the player can switch as the connection changes. The playlists and segments are ordinary files served over HTTP. Those segments need a container too. My pipeline puts H.264 in MPEG-TS (`.ts`) files and AV1 in fragmented MP4 (`.m4s`) files. The AV1 playlist also points to an `init.mp4` file, which the player reads to initialize playback before loading the segments. ## From a recording to public URLs A Vite plugin handles the preparation and makes the files available in dev and production. Adding a video now looks like this: ```text src/data/videos/my-post/ walkthrough.mp4 the recording walkthrough.vtt reviewed English captions, optional walkthrough.poster.webp a poster I picked, optional walkthrough.json {"posterAt": 3}, optional ``` The path becomes the video ID. That recording gets a master playlist at `/videos/my-post/walkthrough/index.m3u8` and a poster beside it. I reference those URLs in the post. **There isn’t a separate packaging command to remember.** Preparing a video before anyone presses play · Changed · Unchanged · Playlists and segments · Recording and optional captions · Build checks its cache · FFmpeg encodes AV1 and H.264 · Finished HLS package and previews · Poster from a picked image, a timestamp or the best frame · Static files on this site · Custom player with native HLS or Mattebox Vite prepares the packages during dev or a build. Playback requests the finished files through dev middleware or the deployed site's asset hosting. ## Vite starts preparation Vite already knows when I’m starting dev or making a build, so I let it start video preparation too. The `videoAssets` plugin lives in `scripts/build/plugins/videos.ts`. With the rest of the app setup left out, this is how I register it: ```ts import { defineConfig } from 'vite'; import { videoAssets } from './scripts/build/plugins/videos.ts'; export default defineConfig(({ isPreview }) => ({ plugins: isPreview ? [] : [videoAssets(process.cwd())], })); ``` Preview serves the completed build and can skip preparation. The app config also passes a separate metadata directory for dev and each build target, so they can run together. Inside the plugin, `prepareVideos` finds the recordings and asks `packageVideo` to prepare anything missing from the cache. It returns a map from public URLs to files on disk. That map is how dev and the build find the finished packages. The work starts in Vite’s `config` [hook](https://vite.dev/guide/api-plugin.html#config), with the returned URL map kept in `assets`: ```ts config: { order: 'pre', async handler(_config, { command }) { assets = await prepareVideos(root, generatedDir, { background: command === 'serve', }); }, }, ``` The source path becomes the video ID, so `walkthrough.mp4` and `walkthrough.mov` would collide. Discovery rejects that pair before starting an encode. It also picks up each recording’s matching VTT captions, poster image and JSON poster setting. **A build waits for every video.** Dev can use what’s already cached while missing packages encode in the background. A new video’s URLs won’t work until its encode finishes. Then its files join the map, and the terminal prints the playlist URL. Posters are prepared the same way but join on their own, so a cached package is served while its poster is still being chosen. When I change a recording, captions or poster, the content watcher restarts dev. An encode already running in the same process survives that restart. Only the current dev instance adds its results to the map. ## FFmpeg builds the streaming package Most of the work happens in `packageVideo`, in `scripts/build/plugins/video-package.ts`. It uses `ffprobe` to read the recording’s dimensions and rotation, then works out which sizes to encode. The targets are 1080p, 720p and 480p, preserving the source’s shape and never upscaling it. Each size gets both an H.264 and an AV1 version, so browsers without AV1 support can use H.264. I use x264 at CRF 20 for H.264 and SVT-AV1 at CRF 30 for AV1. CRF is the encoder’s quality setting. These settings assume SDR recordings, and the machine running the build needs FFmpeg with both encoders, plus `ffprobe`. The cuts between segments have to line up for quality switching to work. I use six-second segments at 30 fps and force a keyframe at each boundary. A keyframe can be decoded without earlier frames, so the player can pick up the next segment from another resolution at the same moment in the recording. FFmpeg writes a media playlist for each version with `-hls_time 6` and `-hls_playlist_type vod`. The player also needs to know how much data each version requires. The packager gets those bandwidth figures from the encoded segment sizes and durations, then writes them into the master playlist alongside the resolution, codec and media-playlist URL. I list AV1 first, though decoder support and the player’s adaptation logic determine what actually plays. Captions need an extra adjustment. I review the transcription and word-level timestamps before saving the VTT file beside the recording. Its cues count from zero, while FFmpeg gives my H.264 segments a later starting timestamp. The build offsets AV1 to match H.264 and adds `X-TIMESTAMP-MAP` to the HLS copy of the captions. With that mapping, captions stay in sync after seeking or switching quality. The build also keeps a clean `captions.vtt` without that mapping for the player’s HTML caption track. ### Choosing a poster The poster used to come from the same encode, at frame zero unless the JSON file named a time. Frame zero is often a blank screen or the start of a fade. Drool’s demo reel opened on black and My Next Filing on an empty paper texture. Altpack’s stop-motion poster was a 984-byte WebP of a plain white frame. ![Two pairs of video posters. Drool's demo reel goes from a solid black frame to a grid of brand and product work. My Next Filing goes from a blank off-white texture to its Clients and payments form.](https://sarthakmishra.com/assets/poster-before-after-antCVIhk.jpeg) Frame zero, left, and the poster I ended up picking for each video. Before changing the build, I tried a few ways of choosing a frame in a throwaway experiment. It sampled four recordings into small frames, measured each one, and ran five methods over the same samples. 1. The current poster, at frame zero or the configured time. 2. FFmpeg’s `thumbnail` filter, which picks the frame closest to the video’s average color histogram. 3. A quality score on one frame every two seconds. 4. The same score on dense samples, every 0.2 or 0.5 seconds. 5. Stable scenes, which favors good frames from shots the video holds for a while. Each method proposed up to three frames, and a review page put them side by side so I could pick one per video. The experiment also checked that the sampled timestamps matched `ffprobe` and that rankings came out the same on every run. ![A grid of first-choice posters from five selection methods for four videos: Altpack, WeKeep, Drool and My Next Filing. The current posters for Altpack, Drool and My Next Filing are blank, while the quality and stable-scene methods show product shots and app screens.](https://sarthakmishra.com/assets/poster-methods-BRzZKU1H.jpeg) Each method's first choice for the four test videos, with its timestamp. Three of the four current posters were blank. WeKeep’s was already set to five seconds, and I kept it. For the other three, I picked a frame from dense sampling each time, though only Drool’s was its first choice. **Dense sampling became the build’s default.** FFmpeg decodes the recording at 160 pixels on its longest side, keeping a frame every 0.2 seconds for clips under 20 seconds and every 0.5 seconds for longer ones. Each frame gets a score from its sharpness, contrast, brightness, color and edge density. Frames in the first and last 4% of the video are skipped, since that’s usually where title cards and fades sit, and so are flat, blank or blurry ones. The winner is extracted from the original recording at its exact timestamp and scaled to fit within 720p. The scoring uses plain pixel measurements, with no model or paid service. The same recording and settings always produce the same poster. My picks for Altpack and My Next Filing weren’t its first choice, so a picked image still wins. A `walkthrough.poster.webp` beside the recording is published as-is, without recompression. Without one, a `posterAt` in the JSON file is honored, including zero. Only videos with neither get the automatic choice. Four videos on the site have a picked image, one has a timestamp, and the other 13 use the frame the build chose. Extracted posters have their own cache under `.cache/video-posters/`, keyed by the recording, the poster choice, the poster code and FFmpeg’s version. ### Seek previews Hovering over the track now shows a thumbnail and the time. FFmpeg generates the thumbnails from the smallest H.264 rendition and puts them in one WebP sprite. It starts with a five-second interval, increasing it for longer videos to keep the sprite to at most 100 frames: ```ts const interval = Math.max(5, Math.ceil(duration / 100)); const frames = Math.ceil(duration / interval); const columns = Math.min(10, frames); const filter = [ 'setpts=PTS-STARTPTS', `fps=1/${interval}:start_time=0:round=up`, 'scale=160:90:force_original_aspect_ratio=decrease', 'pad=160:90:(ow-iw)/2:(oh-ih)/2', `tile=${columns}x${Math.ceil(frames / columns)}`, ].join(','); ``` That filter goes to FFmpeg’s `-vf` option, with `-frames:v 1 -c:v libwebp -quality 60`. The player gets the interval and grid dimensions from generated metadata, then crops the sprite to the tile for the pointer’s time. The sprite loads when someone first opens a preview. Previews have their own cache too, so changing their size or sampling interval doesn’t re-encode the HLS video. ## Reusing completed packages Encoding 20 minutes of video in two codecs takes long enough that I want the next build to reuse the work. Each finished package gets a cache key made from the recording, the tools that encoded it and the caption text: ```ts const fingerprint = createHash('sha256') .update([inputHash, tools, captions].join('\n')) .digest('hex'); ``` The `tools` part includes the packager and rendition code hashes, plus the output of `ffmpeg -version`. That catches changes to encoder settings even when the recording hasn’t changed. If all those inputs match, the package under `.cache/video-packages/files//` is ready to use again. The poster time used to be part of that key, so moving a poster by a second re-encoded every rendition of that video. Now each recording is hashed once, and that hash feeds both the package and poster caches. Changing a poster only touches the poster. A fresh encode goes into a staging directory first. The packager checks that it decodes, that the segments line up, and that every file fits the host’s size limit. Only then does it rename the directory into the cache. A failed encode leaves no completed package for the site to pick up. ## Serving files in dev and production During dev, the files can stay in the cache. Middleware added through Vite’s `configureServer` [hook](https://vite.dev/guide/api-plugin.html#configureserver) looks up each requested video URL and serves the matching file with the correct media type. There’s no extra copy sitting in `public`. Deployment needs those files in the build output. The plugin’s `writeBundle` hook copies them there, preserving the paths used in dev: ```ts writeBundle: { order: 'pre', sequential: true, handler() { if (this.environment.name !== 'client') return; const output = path.resolve(root, this.environment.config.build.outDir); for (const [url, file] of assets) { const target = path.join(output, url.slice(1)); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.copyFileSync(file, target); } }, }, ``` The [environment check](https://vite.dev/guide/api-environment-plugins#accessing-the-current-environment-in-hooks) keeps the copy in the client build, where the public files belong. A post can use the same `/videos/…` URLs while I’m editing it and after deployment. Only packages for current source files enter that map. Removing a recording leaves its old cache entry alone, but excludes it from the next build. Cloudflare’s static assets ignored byte-range requests, which players use to ask for part of a file, often when seeking. I added a Worker handler to return those bytes. The plugin writes the file-size metadata it needs, and dev uses the same handler so seeking works the same way in both places. ## Playing the generated package I first used Mux’s [open-source player](https://github.com/muxinc/elements) with a `src` pointing at the generated playlist. It can play self-hosted HLS, so I got its controls without uploading the video to Mux. The player chunk was 287.2 KB gzipped, even with tracking disabled and its analytics SDK removed from the build. I wanted to keep playback, seeking, captions, speed and fullscreen, with more control over what I was loading. I replaced it with custom React controls, native HLS where available, and [Mattebox](https://github.com/jboix/mattebox) as the fallback engine. [Table of contents debug mode: color-coded overlays show the focus region, content block boundaries, and active intersections while the page scrolls.](https://sarthakmishra.com/videos/building-perfect-toc-component/debug-mode/native.m3u8) The player lives in `src/features/video-player`. Its controls use the site’s shadcn-style buttons, sliders and popovers, with Lucide icons. Speed, quality and captions share a settings panel. Desktop keeps volume beside it, while phones move volume into the panel too. The video, poster and captions are now in the initial HTML, with the browser’s own controls. On a browser with native HLS support, someone can press Play before JavaScript loads, or with it turned off: ```tsx ``` The wrapper waits until the video is close to the viewport before importing the custom controls. They attach to the same video element, keeping its current time, volume, speed and caption selection. The native source is left alone if it’s already set, since assigning it again would restart loading. Autoplay demos still wait until the custom player loads, so videos further down a post don’t all start downloading at once. With quality set to Auto, the initial source choice looks like this: ```ts const native = Boolean( video.canPlayType('application/vnd.apple.mpegurl') || video.canPlayType('application/x-mpegURL'), ); if (native) { if (video.getAttribute('src') !== nativeSrc) video.src = nativeSrc; } else { video.removeAttribute('src'); video.load(); const { createEngine } = await import('./mattebox-engine'); const engine = createEngine(); const attached = engine.attach(video); if (caption) video.append(caption); await attached; engine.load(new URL(src, document.baseURI).href, { start: 0 }); } ``` Native HLS lets the browser load the playlists, buffer segments and choose a quality. The controls still use the video element’s APIs for seeking, volume and speed. This path doesn’t download a JavaScript streaming engine. The `nativeSrc` URL points to `native.m3u8`, an H.264-only version of the master playlist. It reuses the existing segments and leaves captions to the HTML track, avoiding duplicate caption choices in the browser’s menu. I added it after a native Chromium path accepted the mixed-codec playlist but reported a roughly 130-second video as only six seconds long. The adapter also falls back if native playback errors, reports an incomplete duration or stays stalled for eight seconds. The fallback module uses Mattebox 0.17.0 with the stages needed by these packages: ```ts import { mattebox } from 'mattebox'; import hlsCmaf from 'mattebox/protocols/hls-cmaf'; import abr from 'mattebox/stages/abr'; import abrCapSize from 'mattebox/stages/abr-cap-size'; import recovery from 'mattebox/stages/recovery'; import cmafTiming from 'mattebox/stages/cmaf-timing'; import codecProbe from 'mattebox/stages/codec-probe'; import mp4Box from 'mattebox/stages/mp4-box'; import tsTransmux from 'mattebox/containers/ts-transmux'; export function createEngine() { const engine = mattebox({ stages: [ hlsCmaf(), abr(), abrCapSize(), recovery(), cmafTiming(), mp4Box(), codecProbe(), tsTransmux(), ], }); engine.on('tracks:changed', () => { const codec = engine.quality.selectable.some((rendition) => rendition.codecs?.startsWith('av01'), ) ? 'av01' : 'avc1'; engine.quality.constrain('video-codec', { codecs: [codec] }); }); return engine; } ``` The codec constraint keeps quality switching within AV1 or H.264. Switching between AV1/fMP4 and H.264/TS stalled the buffer transition during development, and each codec already has all the resolutions it needs. Choosing a manual resolution also loads Mattebox, since the native video API doesn’t expose a way to pin an HLS rendition. Once attached, the engine applies the selection: ```ts if (quality === 'auto') { engine.quality.auto(); } else { const rendition = engine.quality.selectable.find( (item) => item.height === quality, ); if (rendition) engine.quality.pin(rendition.id); } ``` The adapter preserves the current time and playback rate during that handoff, and checks whether the reader still wants playback before resuming. Returning to Auto keeps the engine attached. The source-choice snippet leaves out that handoff, error handling and cleanup. Both paths use the same clean caption track. Mattebox clears the video’s children when it attaches, so `caption` in the snippet is the existing track node, restored immediately afterwards. React reads its active cues to render captions, with native captions used when the browser opens its own video fullscreen view. The player also keeps the seek previews and keyboard controls. Controls and cursor hide together after two seconds of inactivity during playback, unless focus, an open menu or a drag keeps them visible. ### Player size The replacement loads in parts. At the end of the Mux replacement, these were the additional gzipped player assets compared with the old 287.2 KB Mux Player chunk: | Playback path | Gzipped JS and CSS | Reduction | | -------------------------------------------- | ------------------ | --------- | | Native HLS, before settings load | 14.6 KB | 94.9% | | Native HLS, with settings | 67.8 KB | 76.4% | | Mattebox with AV1 and settings | 106.9 KB | 62.8% | | Mattebox with H.264, its worker and settings | 113.6 KB | 60.5% | | All fallback modules and settings | 119.6 KB | 58.3% | **The native path is 94.9% smaller before settings load.** Settings add another 53.2 KB, fetched on hover, focus or a press of the settings button. The last row counts both the H.264 worker and its main-thread alternative, so it is a conservative total. Extra copies used to retry failed imports aren’t part of these successful playback paths. Uncompressed, the old player chunk was 1,018.0 KB. The native path adds 41.2 KB, and the total with all fallback modules and settings is 342.9 KB. These figures use gzip level 9 and decimal KB on saved optimized Vite builds, comparing Mux Player 3.14.0 in the production output with the replacement’s local-target output. They count player JS and CSS added after the shared code for `/work/my-next-filing` is loaded, excluding media and fonts. The shared vendor chunk was identical. The surrounding wrapper grew from 1.5 KB to 2.9 KB gzipped, outside these totals. They’re measurements of this site’s integration, not a standalone package size. They predate the server-rendered video described above, whose bundle size hasn’t been remeasured. ## What the pipeline produces The whole library is 18 videos, about 20 minutes of footage. That’s 152 MiB of recordings, turned into roughly 500 MiB across 1,291 files, including the preview sprites and native playlists. **The largest single file is 4.7 MiB**, well under Cloudflare’s 25 MiB cap. The AV1 media came out **36% smaller** than H.264 at my settings. Individual videos ranged from 18% to 47% smaller. Those measurements cover every prepared size, including audio and container overhead. | Generated media | Stored size | | ------------------------------------------------- | ----------- | | H.264 HLS media, all sizes | 304.9 MiB | | AV1 HLS media and initialization files, all sizes | 194.2 MiB | That compares file sizes only. The CRF quality settings aren’t comparable across encoders. I haven’t measured whether the two outputs look identical. Check small text in your own recordings before trusting the savings. AV1 also takes noticeably longer to encode in my builds. Keeping both versions makes the library on disk bigger. What a viewer downloads still depends on the codec and quality selected, buffering, and how long they watch. ## What I still have to maintain There’s no playback dashboard. Server logs tell me a segment was requested, not whether someone’s video kept buffering. Earlier versions included a separate MP4 fallback. Keeping those files under Cloudflare’s 25 MiB cap meant cutting some recordings to 480p. I removed that fallback and kept HLS, which splits the higher-resolution versions into smaller files. Native HLS playback and the poster work without JavaScript. The custom controls and Mattebox fallback still need it. If the custom controls fail to load, the browser’s controls remain available, with a retry option below the video. I’ve done my best to test every scenario I could think of on the devices I have access to, but I’m sure I’ve missed some edge cases. That’s a reliability tradeoff I was willing to make when I switched from Mux Player to my own player. Every video request also runs through the Worker that handles byte ranges, so delivery isn’t free compute. Public video URLs can change their contents when I replace a recording, even though the local cache uses hashes. Those URLs can’t safely get a long-lived immutable cache policy. If you need user uploads, live streams, private or paid videos, or real playback analytics, use a platform. That’s a lot of work this pipeline doesn’t do. For a portfolio with 20 minutes of video, this is more machinery than the problem strictly needed. But adding a video is now a file in a folder, and everything a viewer sees is mine to change. I’m happy to maintain it for that. This post was last updated on November 3, 2026. ## Media sources ### Preparing a video before anyone presses play ```mermaid flowchart TD A[Recording and optional captions] --> B[Build checks its cache] B -->|Changed| C[FFmpeg encodes AV1 and H.264] B -->|Unchanged| D[Finished HLS package and previews] C --> D A --> P[Poster from a picked image, a timestamp or the best frame] D --> E[Static files on this site] P --> E E -->|Playlists and segments| F[Custom player with native HLS or Mattebox] class C yellow class D green class P green class F blue ``` ## Structured data ```json { "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": "https://sarthakmishra.com/#website", "url": "https://sarthakmishra.com/", "name": "Sarthak Mishra", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "inLanguage": "en" }, { "@type": "Person", "@id": "https://sarthakmishra.com/#person", "name": "Sarthak Mishra", "url": "https://sarthakmishra.com/about", "jobTitle": "Design Engineer", "image": "https://sarthakmishra.com/og-images/sarthak-mishra.webp", "sameAs": [ "https://github.com/SarthakMishra", "https://www.linkedin.com/in/itssarthakmishra/", "https://x.com/MishraMishry" ] }, { "@type": "WebPage", "@id": "https://sarthakmishra.com/blog/adaptive-video-streaming#webpage", "url": "https://sarthakmishra.com/blog/adaptive-video-streaming", "name": "Adaptive video with FFmpeg, Vite and a custom player", "description": "How I turn recordings into cached AV1 and H.264 streams with FFmpeg and Vite, then play them with native HLS or Mattebox behind custom React controls.", "inLanguage": "en", "isPartOf": { "@id": "https://sarthakmishra.com/#website" }, "mainEntity": { "@id": "https://sarthakmishra.com/blog/adaptive-video-streaming#content" }, "breadcrumb": { "@id": "https://sarthakmishra.com/blog/adaptive-video-streaming#breadcrumb" } }, { "@id": "https://sarthakmishra.com/blog/adaptive-video-streaming#content", "url": "https://sarthakmishra.com/blog/adaptive-video-streaming", "name": "Adaptive video with FFmpeg, Vite and a custom player", "description": "How I turn recordings into cached AV1 and H.264 streams with FFmpeg and Vite, then play them with native HLS or Mattebox behind custom React controls.", "mainEntityOfPage": { "@id": "https://sarthakmishra.com/blog/adaptive-video-streaming#webpage" }, "author": { "@id": "https://sarthakmishra.com/#person" }, "datePublished": "2026-11-01T00:00:00.000Z", "dateModified": "2026-11-03T00:00:00.000Z", "inLanguage": "en", "keywords": [ "video", "ffmpeg", "hls", "av1", "mattebox", "cloudflare" ], "@type": "BlogPosting", "headline": "Adaptive video with FFmpeg, Vite and a custom player", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "image": "https://sarthakmishra.com/og-images/blog/adaptive-video-streaming.webp" }, { "@type": "BreadcrumbList", "@id": "https://sarthakmishra.com/blog/adaptive-video-streaming#breadcrumb", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://sarthakmishra.com/" }, { "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://sarthakmishra.com/blog" }, { "@type": "ListItem", "position": 3, "name": "Adaptive video with FFmpeg, Vite and a custom player", "item": "https://sarthakmishra.com/blog/adaptive-video-streaming" } ] } ] } ``` --- title: "How to scope a SaaS project you can afford to finish" url: "https://sarthakmishra.com/blog/scoping-a-saas-project" description: "How I approach SaaS scope and pricing as a former agency owner and current freelancer, and what to ask when budgeting for design and development." image: "https://sarthakmishra.com/og-images/blog/scoping-a-saas-project.webp" --- [View HTML](https://sarthakmishra.com/blog/scoping-a-saas-project) · [Site index](https://sarthakmishra.com/llms.txt) October 31, 2026 # How to scope a SaaS project you can afford to finish How I approach SaaS scope and pricing as a former agency owner and current freelancer, and what to ask when budgeting for design and development. [Let's work together](https://sarthakmishra.com/blog/scoping-a-saas-project#inquiry) ![Gold coins and a clock spiral into a deep blue vortex.](https://sarthakmishra.com/assets/scoping-a-saas-project-hero-B8usj6Vr.jpeg) If you’re budgeting for a SaaS project, a list of screens will only get you so far. Before I can give you a useful quote, I need to understand what people will do with the product and how much of it already works. **Working that out together is part of the job.** You shouldn’t need to become a product designer before you can hire one. When I ran Drool, my design agency, I said yes to too much. “Sure, we’ll handle that too” became a habit, and the scope and timelines got hard to control. I wrote about it when I [shut the agency down](https://sarthakmishra.com/blog/drool-is-shut-down). I helped create those expectations. I freelance now, doing design and development. I’d rather spend more time with you deciding what the project includes before either of us commits to a price. That gives you something you can budget for and gives me a job I can finish. ![My Next Filing's plan screen, with a next-action card, a payment deadline, and links to the official tax portal and supporting sources.](https://sarthakmishra.com/assets/mynextfiling-next-action-B2lW6bmM.jpeg) [My Next Filing](https://sarthakmishra.com/work/my-next-filing) leads with the next task, its deadline, and the action to take. ## Start with what you need the product to do You can come to me with a rough idea, an existing app, or a folder full of screenshots. What helps most is explaining the problem you want to solve. Suppose you run a SaaS product for agencies. Teams upload drafts, then chase clients over email to find out whether the work is approved. You want that decision inside the app so nobody has to search an email thread before sending the final file. That’s a useful starting point. “Add an approval screen” leaves me guessing at much of the work. I’d ask who can approve a draft, whether several people need to agree, and what happens when someone uploads a replacement. One person clicking Approve is a smaller job than five people reviewing different versions and waiting on each other. Both might look like one screen in your brief. As a designer, I need to make those choices understandable on the screen. As a developer, I need to make sure the app saves the right decision against the right file. If an approval carries over to a draft the client hasn’t seen, a polished interface won’t help. For this hypothetical project, suppose you decide that one named person can approve or request changes. Feedback can be plain text, and a replacement draft needs a new decision. We can sketch that before spending time on the visual design. A client approval belongs to a particular draft · Approve · Request changes · Team shares a draft · Named client reviews that version · Client decision · Record approval against this version · Record feedback against this version · Team uploads a revised draft · Team sees the approved version Replacing a draft requires a new decision. The previous decision stays attached to the version the client actually reviewed. You don’t need to answer every question in the first call. I do need your help deciding which behavior matches how your customers work. Those decisions tell me what I’m estimating. ## Show me what we’re starting with The approval feature could be an addition to a working product or the first piece of a new one. That changes the quote considerably. If accounts, client access, and file storage already work, we may be able to use them. A walkthrough with whoever maintains the product helps me check that. An upload that works for your staff might give clients no way to open the file. Fixing that belongs in the estimate too. The same goes for design. On [SellScale](https://sarthakmishra.com/work/sellscale), our team at Drool built a modular design system for a product with more than 200 screens. Shared buttons, inputs, and other components gave us parts we could use across the product. ![SellScale's Figma component library, with variants for buttons, button groups, chips, and checkboxes.](https://sarthakmishra.com/assets/Sellscale-case-study-3-C4Fz6BDe.jpeg) Part of the design system we built for SellScale. Screens could share these components. That screenshot is a Figma library. A design-and-development quote also depends on which components exist in code. A finished design can save design time while still leaving the implementation to do. Share what you have, including the awkward parts. A prototype, old design files, customer complaints, and a conversation with your engineer help me judge what we can reuse and what needs work. If an unknown could change the price substantially, I’d suggest a small paid investigation before quoting the whole job. Agree on its cost and what you’ll get from it. For this example, that might be confirmation that the existing app can support client access, followed by a scope and estimate you can use. You should be able to decide whether to continue when that work ends. ## Ask what the fee gets you Once the scope is clearer, I separate the work we can reuse from the work the project adds. I look at the flows and screen states that need designing, the components that need building, and the connections to the existing product. Feedback, testing, and handover need time too. If several screens use the same new table component, the estimate includes that component once and the work needed to use it on each screen. Each use still needs the right data and behavior. A version-comparison view adds design and implementation work that a plain approval status doesn’t. Those differences help me estimate the effort behind the fee. Two quotes can use the same feature name while assigning different work to your team. Check who owns each part. | Part of the job | What to check in the quote | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Design | Does it include working out the flow, phone layouts, and what people see when something fails? Which files or prototypes will you receive? | | Frontend development | Will the screens work in the browser and connect to real data? Who handles that connection? | | Backend development | Who makes sure decisions are saved and only the right people can see or change them? | | Testing and release | Who checks the complete feature, puts it live, and gives your team access to the files and code? | | Reviews and support | How much feedback is included? How long will someone be available to fix problems after release? | If you’re commissioning design only, your engineers still need time to build it. If I’m building the interface and your team owns the backend, we need to agree on what they’ll provide and when. **Work assigned to your team still belongs in your budget.** Even with the same scope, people will quote different fees. Experience, rates, and how they run their business affect the price. An agency may include coordination and specialist time that a freelancer’s quote doesn’t. Ask who is doing the work and what responsibility they’re taking. Price alone won’t tell you that. The billing arrangement matters too. With a fixed fee, we agree on the work and what counts as finished. I allow for uncertainty within that scope when setting the price. Once you accept the quote, the fee stays fixed for the agreed work. If that work takes me longer than I expected, the extra time is mine to cover. With hourly or daily billing, you pay for the time used. Ask for an estimate, a spending limit, and a point where work stops for your approval. Before comparing totals, have each person mark their assumptions and exclusions. If one expects your engineer to connect the screens and another includes that work, you have a difference you can account for. I’ve covered choosing the person in more detail in my [freelance designer hiring guide](https://sarthakmishra.com/blog/hiring-a-freelance-designer). ![Learnway's learning-path browser with a search field, topic filters, cards for different subjects, and a path-generation status message.](https://sarthakmishra.com/assets/learnway-explore-BsnsVuVL.jpeg) [Learnway](https://sarthakmishra.com/work/learnway). Search, topic filters, and path generation share a screen, with different work behind each. ## Share the budget while we can still change the scope I understand the hesitation to share a budget. You want to find out what the work costs without every quote landing at your maximum. It’s reasonable to ask for an initial range and what it assumes. When we’re deciding what to build, though, knowing your limit helps. I can suggest a smaller version, use more of what you already have, or tell you that the project won’t fit. I’d rather have that conversation before designing something you can’t afford to develop. [Basecamp’s Shape Up](https://basecamp.com/shapeup/1.2-chapter-03) starts with how much time a problem deserves, then shapes the solution to fit. I like bringing that constraint into a budget conversation. There are usually choices to make before every feature becomes a commitment. Suppose your approval wish list includes several approvers, comments pinned to an image, automatic reminders, and a screen for comparing versions. If the quote is beyond your budget, I’d discuss cuts like these with you. | Requested capability | Smaller version | What the team gives up | | ------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------- | | Several people must approve | One named approver per project | The client coordinates their own internal agreement. | | Comments pinned to the draft | One text field for feedback | People describe where the change belongs. | | Automatic reminder emails | Staff send a reminder themselves | Someone has to do the chasing. | | Browse and compare old versions | Keep each decision’s draft reference, without a comparison screen | Reviewing changes between versions stays outside this feature. | You still get a client approval feature. Your team takes on the reminders, and the client coordinates their internal agreement. Those are real compromises. If someone will spend hours chasing approvals every week, keeping the reminders may be worth more to you than another part of the project. I’d keep the work needed to save decisions correctly, prevent clients from opening each other’s files, and recover when a save fails. Phone layouts and keyboard access belong in the agreed design too. **I’d cut a feature before leaving the remaining flow half working.** ![MyDrip wireframes showing preference questions, photo uploads, selection states, and the steps connecting those screens.](https://sarthakmishra.com/assets/mydrip-lowfi-wireframes-BG2XrRL5.jpeg) Early wireframes for [MyDrip](https://sarthakmishra.com/work/mydrip), working through preferences and photo uploads before the finished visual design. For an illustration of the budget, suppose the fuller version is quoted at $10,000 and the smaller one at $7,000. These are **invented amounts in USD**, not my rates or a price for this feature. Assume the existing accounts and file storage have been checked and can be reused. Your total allowance might look like this, with some money held back for additions or costs outside the quote. | Budget | Fuller version | Smaller version | | ---------------------------------------------------------------- | -------------- | --------------- | | Agreed design and development fee, including testing and release | $10,000 | $7,000 | | Money you hold back | $2,000 | $1,000 | | Total available | **$12,000** | **$8,000** | The lower fee follows a reduction in work. It still takes time to understand the app and get the remaining feature ready for customers. You can’t assume that removing half the items from a wish list will halve the quote. The money held back stays under your control. It can cover separately agreed additions, such as a new integration, or costs explicitly excluded from the quote. Spending it requires your approval. The fixed fee already covers delivery of the agreed work, including the uncertainty I allowed for when pricing it. These amounts don’t prescribe a percentage. If an unresolved dependency could change the scope, investigate it before accepting a fixed quote. You can also ask about a discount. I may lower a fee when there’s a good prospect of more work with a client, depending on what I know about the relationship. A mention of future projects doesn’t automatically earn a discount. If the price changes, write down whether the work changed too. A discounted fee still buys the same agreed work unless we explicitly change the scope. ## Budget for getting it live and keeping it running The example above adds a feature to an existing web app. If you’re commissioning a new SaaS product, accounts, billing, file storage, and other basic functions may all need to be built or connected. A marketing website is another piece of work. Don’t use a feature budget as a budget for the whole product. Ask for recurring costs alongside the delivery fee. Hosting, email, storage, and other services can keep charging after the project ends. Maintenance needs an owner and an allowance too. Confirm what the initial support period covers and how later work will be priced. There may also be work for you before launch. Someone has to supply content, set up service accounts, and review what has been built. If your engineers are involved, include their availability in the schedule. For a website, be specific about who writes the copy and supplies the images. Finished layouts can still be waiting on a lot of content. ![Phidata's landing page shown on desktop and mobile, including the mobile navigation menu and a section explaining the product's tools.](https://sarthakmishra.com/assets/Phidata-case-study-6-CSHOC5x7.jpeg) [Phidata’s landing page](https://sarthakmishra.com/work/phidata), with desktop, mobile, and navigation layouts included in the design work. Agree on milestones and payment dates so you know when the money is needed. Each review should have a clear decision to make, and someone on your side who can make it. If several people need to contribute, leave time to collect their feedback before sending it back. ## Agree on changes before paying for them Suppose you see the first version and realize you need a second approver. That’s a reasonable thing to discover. It also changes the job. The app now needs to handle one person approving while another requests changes. It needs to show who is still waiting and when the draft counts as approved. Adding a second name on the screen is a small part of that work. I’d explain what changes in the estimate and schedule before starting it. You could replace something else in the scope, approve the additional cost, or leave it for later. You should get to choose. That doesn’t mean every piece of feedback is extra work. If we agreed that approval belongs to a particular draft and the app saves it against the wrong one, the delivered behavior is wrong. Fixing it is part of meeting the agreement. **Ask what counts as a revision, a defect, or an addition before work starts.** The review allowance and support period should be clear too. ## Bring enough to start a conversation You don’t need to send me a document that answers every technical question. For the approval example, this would be enough to start. > We have a working project app, but our customers still approve drafts over email. We want them to approve or request changes in the app, and we need to know which version they reviewed. > > We have accounts and file uploads already. Our engineer can walk you through them. One approver and text feedback would be enough for now, and our team can send reminders manually. > > We have up to $8,000 available for this work, including some room for unknowns. We need help with design and development. Please explain what you’d include, what you’d need from our team, and whether this can fit before we commit. Add your deadline and why it matters. Show me what exists, tell me what you’re unsure about, and name the person who will make decisions with me. I can help turn that into a scope and a quote you can judge. ## Work with me I work with founders and product teams on SaaS UI/UX, website design, and React frontend development. I can help define the scope, design the flows, and build the interface on your team’s backend. If the product needs reusable components, I can include a [design system](https://sarthakmishra.com/services/design-systems) in the work. Bring the problem, what you already have, and any budget or deadline you’re working within. I’ll help work out what fits, what can wait, and what needs investigating before I can quote. Rough ideas and sketches are enough to start that conversation. Use the [inquiry form below](https://sarthakmishra.com/blog/scoping-a-saas-project#inquiry) to tell me what you’re trying to get done, and we can work out the next step together. ## Media sources ### A client approval belongs to a particular draft ```mermaid flowchart TD A[Team shares a draft] --> B[Named client reviews that version] B --> C{Client decision} C -->|Approve| D[Record approval against this version] C -->|Request changes| E[Record feedback against this version] E --> F[Team uploads a revised draft] F --> B D --> G[Team sees the approved version] class D green class E orange ``` ## Structured data ```json { "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": "https://sarthakmishra.com/#website", "url": "https://sarthakmishra.com/", "name": "Sarthak Mishra", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "inLanguage": "en" }, { "@type": "Person", "@id": "https://sarthakmishra.com/#person", "name": "Sarthak Mishra", "url": "https://sarthakmishra.com/about", "jobTitle": "Design Engineer", "image": "https://sarthakmishra.com/og-images/sarthak-mishra.webp", "sameAs": [ "https://github.com/SarthakMishra", "https://www.linkedin.com/in/itssarthakmishra/", "https://x.com/MishraMishry" ] }, { "@type": "WebPage", "@id": "https://sarthakmishra.com/blog/scoping-a-saas-project#webpage", "url": "https://sarthakmishra.com/blog/scoping-a-saas-project", "name": "How to scope a SaaS project you can afford to finish", "description": "How I approach SaaS scope and pricing as a former agency owner and current freelancer, and what to ask when budgeting for design and development.", "inLanguage": "en", "isPartOf": { "@id": "https://sarthakmishra.com/#website" }, "mainEntity": { "@id": "https://sarthakmishra.com/blog/scoping-a-saas-project#content" }, "breadcrumb": { "@id": "https://sarthakmishra.com/blog/scoping-a-saas-project#breadcrumb" } }, { "@id": "https://sarthakmishra.com/blog/scoping-a-saas-project#content", "url": "https://sarthakmishra.com/blog/scoping-a-saas-project", "name": "How to scope a SaaS project you can afford to finish", "description": "How I approach SaaS scope and pricing as a former agency owner and current freelancer, and what to ask when budgeting for design and development.", "mainEntityOfPage": { "@id": "https://sarthakmishra.com/blog/scoping-a-saas-project#webpage" }, "author": { "@id": "https://sarthakmishra.com/#person" }, "datePublished": "2026-10-31T00:00:00.000Z", "inLanguage": "en", "keywords": [ "saas", "project-scope", "design-budget", "web-app-development", "product-design" ], "@type": "BlogPosting", "headline": "How to scope a SaaS project you can afford to finish", "publisher": { "@id": "https://sarthakmishra.com/#person" }, "image": "https://sarthakmishra.com/og-images/blog/scoping-a-saas-project.webp" }, { "@type": "BreadcrumbList", "@id": "https://sarthakmishra.com/blog/scoping-a-saas-project#breadcrumb", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://sarthakmishra.com/" }, { "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://sarthakmishra.com/blog" }, { "@type": "ListItem", "position": 3, "name": "How to scope a SaaS project you can afford to finish", "item": "https://sarthakmishra.com/blog/scoping-a-saas-project" } ] } ] } ``` --- title: "Optimizing the sh*t out of TanStack Start" url: "https://sarthakmishra.com/blog/optimizing-tanstack-start" description: "A practical TanStack Start performance guide: how cutting downloads and fixing fonts and images took my home page from 73 to 100 on mobile Lighthouse." image: "https://sarthakmishra.com/og-images/blog/optimizing-tanstack-start.webp" --- [View HTML](https://sarthakmishra.com/blog/optimizing-tanstack-start) · [Site index](https://sarthakmishra.com/llms.txt) October 26, 2026 # Optimizing the sh\*t out of TanStack Start A practical TanStack Start performance guide: how cutting downloads and fixing fonts and images took my home page from 73 to 100 on mobile Lighthouse. [Let's work together](https://sarthakmishra.com/blog/optimizing-tanstack-start#inquiry) ![Colorful illustration of a central palm tree icon surrounded by charts, interface controls, and hands.](https://sarthakmishra.com/assets/optimizing-tanstack-start-hero-CJdqb0XM.jpeg) A few days after I [moved this site to TanStack Start](https://sarthakmishra.com/blog/astro-to-tanstack-start), the home page scored 73 on mobile PageSpeed Insights. Its largest contentful element was a paragraph, with a reported paint time of **5.2 seconds**. That’s a long wait for a paragraph. I worked through the site with [Claude Code and Codex](https://sarthakmishra.com/blog/optimizing-tanstack-start#how-i-used-claude-code-and-codex), tracing what the browser downloaded and what kept it from painting. A card component was sending the whole image catalog to every page. Closed dialogs loaded their code anyway. Phones downloaded images for navigation they couldn’t see. Most of the useful fixes came from removing work I’d accidentally given the browser. By October 30, the home page scored **100, with LCP down to 0.9 seconds**. I also built a critical CSS pipeline and delayed hydration. Those took much more work, and I still can’t tell you how much they contributed to the score. | Mobile home-page metric | [October 24, before the fixes](https://pagespeed.web.dev/analysis/https-sarthakmishra-com/adninyd8c2?form_factor=mobile) | [October 30, latest](https://pagespeed.web.dev/analysis/https-sarthakmishra-com/x5yjk1zd4x?form_factor=mobile) | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | Performance | 73 | 100 | | First Contentful Paint | 2.3 s | 0.9 s | | Largest Contentful Paint | 5.2 s | 0.9 s | | Total Blocking Time | 160 ms | 0 ms | | Cumulative Layout Shift | 0.002 | 0.002 | | Speed Index | 4.5 s | 0.9 s | These are individual lab runs on the live home page, with several changes between them. They show where I started and ended, not what each fix contributed. The later run was captured on October 30 at 10:43 PM IST. ![PageSpeed Insights mobile report for sarthakmishra.com: Performance, Accessibility, Best Practices and SEO all score 100. FCP is 0.9 seconds, LCP 0.9 seconds, TBT 0 milliseconds, CLS 0.002 and Speed Index 0.9 seconds. The report uses Lighthouse 13.6.0, an emulated Moto G Power and Slow 4G.](https://sarthakmishra.com/assets/pagespeed-home-mobile-2026-10-30-C9vUehQp.jpeg) The live home page on mobile, October 30, 2026. ![PageSpeed Insights desktop report for sarthakmishra.com: Performance, Accessibility, Best Practices and SEO all score 100. FCP is 0.3 seconds, LCP 0.3 seconds, TBT 0 milliseconds, CLS 0.001 and Speed Index 0.4 seconds. The report uses Lighthouse 13.6.0 with desktop emulation.](https://sarthakmishra.com/assets/pagespeed-home-desktop-2026-10-30-AOGjNDw9.jpeg) The same run on desktop. If you’re working on a similar app, I’d start with [unnecessary JavaScript](https://sarthakmishra.com/blog/optimizing-tanstack-start#stop-loading-the-rest-of-the-site), [fonts](https://sarthakmishra.com/blog/optimizing-tanstack-start#smaller-fonts-made-the-text-jump), and [hidden images](https://sarthakmishra.com/blog/optimizing-tanstack-start#phones-were-downloading-the-desktop-navigation). The [CSS and hydration experiments](https://sarthakmishra.com/blog/optimizing-tanstack-start#critical-css-gave-me-less-blocking-css-and-more-upkeep) are further down, along with what broke. ## Find out what the paragraph is waiting for The document arrived in about 110 ms and already contained the paragraph. There was no client-side data fetch to wait for. That narrowed the investigation to what happened in the browser. **Find your LCP element before optimizing anything.** Mine was text already in the document, so I started with fonts, blocking CSS, and downloads competing with them. The [Performance score](https://developer.chrome.com/docs/lighthouse/performance/performance-scoring) comes from measured timings. This is where I’d start for each slow metric. | What you see | Where I’d look first | | ----------------------------------------------------- | ----------------------------------------------------------------------------- | | The document itself arrives late | Server response time, route loaders, sequential data requests, caching | | First Contentful Paint (FCP) is late | Render-blocking CSS, font loading, work before the first frame | | Largest Contentful Paint (LCP) is much later than FCP | The LCP element, when its resource is discovered, and what delays its display | | Total Blocking Time (TBT) is high | Long JavaScript tasks, hydration, third-party scripts, animation loops | | Cumulative Layout Shift (CLS) is high | Font swaps, missing media dimensions, content inserted above existing content | | Speed Index is high | Filmstrip frames, late visible sections, animation during the initial load | Where to look once you know the LCP element · No · Yes · Image · Text · LCP element · In the HTML response? · Route loaders, prerendering, server reads · Image or text? · Discovery, priority, sizes · Fonts and render-blocking CSS · JavaScript and hidden downloads competing for bandwidth Check the document response first. If the content is there but appears late, the cause is in the browser. ### Check the response before React changes it Open the document request in DevTools and search its Response tab for your heading and main text. The DOM after hydration doesn’t tell you what the server sent. ![Chrome DevTools with the document request selected and its Response tab open. A search for the h1 tag highlights the server-rendered heading, with the introduction paragraphs visible below.](https://sarthakmishra.com/assets/devtools-html-response-D3fw9poJ.jpeg) Select the document request, open Response, and search for your heading or main text. This is the HTML the server sent, before client rendering changes the DOM. If the content is missing, fetch it in a route loader. Start’s [execution model](https://tanstack.com/start/latest/docs/framework/react/guide/execution-model) allows loaders to run on the server and in the browser, so server-only code still needs a server boundary. Moving an import into a loader doesn’t keep it out of the client bundle. For public pages that can be generated at build time, [static prerendering](https://tanstack.com/start/latest/docs/framework/react/guide/static-prerendering) puts the content in the document. This configuration prerenders two routes. ```typescript tanstackStart({ prerender: { enabled: true, autoStaticPathsDiscovery: false, crawlLinks: false, }, pages: [ { path: '/', prerender: { enabled: true } }, { path: '/about', prerender: { enabled: true } }, ], }); ``` Personalized pages still need request-time rendering. Check the deployed [Content-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding) and [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) headers too. Content-hashed assets can have a long cache lifetime. HTML needs one that fits how often it changes. ### Make sure the measurements are comparable Use a production build. Development mode includes work visitors never receive. Repeat the audit with the same settings, compare medians, and confirm on the deployed site. On October 25, the same build scored 91, 95, and 92 within a few minutes. PageSpeed also reused cached audits under new report links, so I had to check capture times before counting a run as new. My local preview caused a different distraction. It served 165 KiB of uncompressed HTML and scored 94 to 95. Brotli took the same document to about 28 KiB and the score to 99, much closer to production. I could have spent a while trying to fix the app for that one. Once the HTML and the test setup were accounted for, the Network panel showed what else the page was asking for. ![Chrome DevTools Network panel showing the document, fonts, stylesheets, and JavaScript requests, with Size, Priority, and Waterfall columns visible.](https://sarthakmishra.com/assets/devtools-network-waterfall-BJ1d4nrW.jpeg) Right-click a Network column heading to enable Priority and Waterfall. Read across each row to see what downloads, its priority, and when it starts. The filter hides data URLs. ## Stop loading the rest of the site A card component imported the full image manifest. That made every page download metadata for 117 images, about **35 KiB compressed**, even when it displayed a handful. The images had their own downloads. These were their descriptions and rendition URLs, shipped as JavaScript. Each route now carries the image data it renders. Navigation fetches newly needed data. I also removed tooling-only excerpts and unused recommendation scores from the shared content index. The home page’s module-preloaded JavaScript fell from roughly 234 to 196 KiB. Its HTML grew from about 42 to 49 KiB because it now contained the selected image data. I was happy with that trade. The unrelated catalog stopped going to every reader. **Check both the loader response and the emitted bundles.** Returning a smaller object won’t help much if an import still puts the full dataset in the browser’s startup code. ![Chrome DevTools Network panel filtered to JavaScript and sorted by Size from largest to smallest, with the main entry bundle and shared chunks at the top.](https://sarthakmishra.com/assets/devtools-javascript-size-S38PTYAC.jpeg) Choose JS and sort Size from largest to smallest to find downloads worth investigating. Transfer size tells you what to inspect; a Performance recording shows what costs CPU time. Filter Network to JS, sort by size, and trace the larger files back to their imports. Compare a simple route with a complex one. Shared downloads often lead back to the root route, a layout, or a component both pages use. ### A closed dialog can still download TanStack Router’s [route code splitting](https://tanstack.com/router/latest/docs/framework/react/guide/code-splitting) separates routes. Within a route, search, the mobile menu, image enlargement, a portrait viewer, and a leave-form confirmation were all adding to my initial load. A lazy component is a start. Declare it at module scope. ```tsx import { lazy } from 'react'; const SearchDialog = lazy(() => import('./SearchDialog')); ``` `SearchDialog` is a default export here. [React requests the module when you first try to render it](https://react.dev/reference/react/lazy). Rendering it with `open={false}` can therefore start the download while the dialog is closed. I kept the triggers available and mounted the dialogs on first open. They need `Suspense`, an error boundary, and enough time to finish closing and return focus before unmounting. Initially, I warmed their chunks after page load so the first interaction wouldn’t wait for a download. That cut the initial JavaScript. | Page | Before, KiB gzip | After, KiB gzip | | ----------- | ---------------- | --------------- | | Home | 196.1 | 174.4 | | A long post | 213.0 | 190.8 | But the warm-up still charged every reader for features most wouldn’t use. Base UI’s dialog engine alone added about 14 KiB gzip after load on every page. I later changed the triggers to load on hover, focus, or press. The first tap on a phone can wait briefly for the chunk. I accepted that. Verify the requests in the browser. A separate chunk hasn’t saved a download if the page immediately preloads it. [Filtering a recorded Chrome DevTools Network log to fonts and JavaScript, then sorting JavaScript requests by size.](https://sarthakmishra.com/videos/optimizing-tanstack-start/network-filters/native.m3u8) In a recorded Network log, choose Font, then JS. Click Size until the largest downloads appear first. This short clip has no audio. ## Smaller fonts made the text jump The normal Inter file was about 342 KiB. Fontsource’s WOFF2 subsets let the browser request only the character ranges a page uses. Removing the optical-size axis then took the normal Latin file from 71.2 to 47.1 KiB, about a third smaller. I kept small symbol supplements for characters the interface still needed. I also cut three font preloads to one, the normal Latin Instrument Serif face used by the heading. Other faces load when text needs them. ![Chrome DevTools Network panel filtered to Font, showing the Inter and Instrument Serif WOFF2 files with their transfer sizes, priorities, and request timing.](https://sarthakmishra.com/assets/devtools-font-requests-DwfjKxNh.jpeg) The Font filter isolates the files this page downloaded. Compare their sizes and start times before changing font imports or preload hints. Check the family, weight, style, and character range of each request. A preload should earn its place by fixing measured late discovery in the initial view. Its URL, `type`, and `crossorigin` mode must match the font request, or you can download it twice. [Google’s font-loading guide](https://web.dev/articles/optimize-webfont-loading) explains the details. The smaller downloads exposed another problem. Text appeared sooner, then moved when the web font arrived. Newsletter Issue 1 recorded CLS around 0.21 during those changes because the heading changed size and pushed everything below it. **A faster font swap still needs a stable fallback.** I adjusted the fallback’s metrics to bring its dimensions closer to Instrument Serif. ```css @font-face { font-family: 'Instrument Serif Fallback'; src: local('Times New Roman'), local('TimesNewRomanPSMT'), local('Liberation Serif'), local('Noto Serif'); size-adjust: 83.75%; ascent-override: 118.2%; descent-override: 37.01%; line-gap-override: 0%; } ``` Put the fallback after the web font in the `font-family` stack. These percentages belong to my pairing, so measure yours. Google’s [CLS guidance](https://web.dev/articles/optimize-cls) covers the overrides. A cold-load recording with the filmstrip visible will show whether the text appears early and stays put. ## Phones were downloading the desktop navigation My desktop navigation was hidden on phones. Its eager images still appeared in the preload list. Nine requests for navigation the reader couldn’t see. Marking the images lazy removed the preloads. The sidebar now waits for hydration and a desktop-sized viewport before decoding its images, so phones never request them. Desktop navigation appears slightly later as a result. Video posters had the same problem. I’d deferred the enhanced player with an `IntersectionObserver` but left the poster eager. The player could wait for scrolling while its poster downloaded immediately. Both needed deferral. ### Give the visible image the priority it needs On a page with an image LCP, make that image discoverable in the initial HTML, eager, and eligible for high fetch priority. [Google’s LCP guidance](https://web.dev/articles/optimize-lcp) separates time to first byte, resource discovery, transfer, and render delay. The Performance panel shows where the time went. ![Chrome DevTools Performance recording with LCP breakdown expanded. It shows 540 milliseconds of time to first byte and 179 milliseconds of element render delay beside the loading timeline.](https://sarthakmishra.com/assets/devtools-performance-lcp-BRJeoNSC.jpeg) Open LCP breakdown in a Performance recording. This page has a text LCP, so the breakdown shows time to first byte and element render delay. An image LCP can also include resource loading phases. For an image in a container capped at 960 pixels, with 16-pixel gutters on smaller screens, the markup looks like this. ```tsx An editor displaying a published article ``` Those paths assume generated 16:9 renditions. The `sizes` hint describes the layout. On my site, which uses different renditions, correcting it let a phone choose a 640-pixel file where it had previously chosen a 1080-pixel one. Images further down can load lazily. Reserve their space with dimensions so they don’t move the page when they arrive. ## Give deferred work a reason to start I had an idle home-page animation updating SVG geometry about 30 times a second. Delaying its start still left that work running. It needed to stop when it had nothing to do. Choose a trigger for each piece of work, then check what it does after that trigger. | Work | A useful starting policy | | ------------------------------------------ | ------------------------------------------------------------ | | Main text, initial styles, LCP resource | Available for the first render | | A video player or heavy embed further down | Prepare as it approaches the viewport | | Search, an editor, or a dialog | Load on intent or first use | | Decorative animation | Start after load, then pause when idle or out of view | | Third-party scripts | Load when the feature needs them, accounting for measurement | For decorative work, this helper waits for `load` and an idle slot. It also works if `load` has already fired. ```typescript export function afterPageLoad(callback: () => void) { let idle = 0; let timer = 0; const schedule = () => { if (typeof requestIdleCallback === 'function') { idle = requestIdleCallback(callback, { timeout: 2000 }); } else { timer = window.setTimeout(callback, 200); } }; if (document.readyState === 'complete') schedule(); else window.addEventListener('load', schedule, { once: true }); return () => { window.removeEventListener('load', schedule); if (idle) cancelIdleCallback(idle); window.clearTimeout(timer); }; } ``` Call it from client-side setup and cancel the pending callback during cleanup. Any loop or subscription it starts needs its own cleanup too. Deferral changes behavior. My analytics loads on first interaction or a few seconds after load, so it can miss a very short visit. Form verification starts on focus, giving it time to load before submission. Test with someone who starts using the page as soon as it appears. ### React Compiler cost bytes I chose to keep I’d already adopted the [native React Compiler](https://oxc.rs/blog/2026-08-18-react-compiler-support.html#vite). Across five alternating baseline and compiled pairs on three routes, timings stayed roughly unchanged while compressed JavaScript grew by 9.5 to 14.1 KiB per route. I kept it because it replaced hand-written memoization. That maintenance benefit was worth the measured byte cost to me. It couldn’t decide whether a closed dialog belonged in the first load, though. Loading changes needed their own measurements. ## Critical CSS gave me less blocking CSS and more upkeep After the unnecessary downloads, I worked on how the stylesheet arrived. This was where the fixes became harder to justify. I first moved feature styles out of the global stylesheet and into their owning routes or components. Start tracks side-effect CSS imports and CSS modules in its route manifest. Explicit `?url` links returned from `head().links` sit outside that manifest, which matters for its built-in delivery features. The [CSS styling guide](https://tanstack.com/start/latest/docs/framework/react/guide/css-styling) covers both. I tried all three delivery strategies. | Strategy | Benefit | Cost to measure | | ------------------------------------------------ | ------------------------------- | ----------------------------------------------- | | External route CSS | Reusable browser cache | A blocking request on a cold visit | | Inline route CSS | Removes that stylesheet request | More HTML on every document load | | Extracted critical CSS plus deferred stylesheets | Fewer rules needed before paint | Extraction complexity and more states to verify | Start’s experimental, production-only option inlines route CSS from its manifest. ```typescript tanstackStart({ server: { build: { inlineCss: true }, }, }); ``` Inlining repeated the full route stylesheet in every document. Moving feature styles out and switching back to external files roughly halved the home page’s HTML. PageSpeed couldn’t separate the results convincingly. The inlined version scored 91 to 95 across three runs. The first two external-CSS runs scored [92](https://pagespeed.web.dev/analysis/https-sarthakmishra-com/ilmnkrz1n9?form_factor=mobile) and 97. Then I used [Beasties](https://github.com/danielroe/beasties) to extract rules matching each prerendered document. Those rules went inline. The complete stylesheets loaded through the `media="print"` and `onload` [pattern](https://web.dev/articles/defer-non-critical-css), with a `