Scalable Responsive Design · How it works
1 min readRapid overview
- How it works
- Part 1: Scalable Frontend Architecture
- What is Frontend Scalability?
- Scalability Dimensions
- 1. Performance Scalability
- 2. Team Scalability
- 3. Maintainability Scalability
- Scalable Code Organization
- Feature-Based Structure
- Layer-Based Architecture
- State Management at Scale
- Local vs Global State
- State Colocation
- Data Fetching at Scale
- Caching and Deduplication
- Pagination and Infinite Scroll
- Performance Optimization Patterns
- Virtualization for Large Lists
- Memoization
- Code Splitting Strategies
- Bundle Optimization
- Analyzing Bundle Size
- Tree Shaking
- Dynamic Imports
- Part 2: Responsive Design
- Mobile-First Approach
- Responsive Breakpoints
- Responsive Hooks
- Responsive Images
- Responsive Typography
- Container Queries
- Responsive Utilities
- Best Practices
- 1. Performance Budget
- 2. Accessibility
- 3. Testing
How it works
Part 1: Scalable Frontend Architecture
What is Frontend Scalability?
Frontend scalability refers to the application's ability to handle:
- User Growth: More concurrent users
- Feature Growth: More features without performance degradation
- Team Growth: More developers working simultaneously
- Data Growth: Larger datasets and state
- Device Diversity: Different screen sizes and capabilities
Scalability Dimensions
1. Performance Scalability
Handle increased load without degradation.
// Bad: Loads everything upfront
import AllComponents from './components';
import AllUtilities from './utils';
import AllPages from './pages';
// Good: Code splitting
const HomePage = lazy(() => import('./pages/HomePage'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
2. Team Scalability
Multiple teams work without blocking each other.
project/
├── packages/
│ ├── design-system/ # Shared components
│ ├── shared-utils/ # Common utilities
│ ├── team-a-features/ # Team A owns these
│ ├── team-b-features/ # Team B owns these
│ └── shell-app/ # Composition layer
3. Maintainability Scalability
Codebase remains manageable as it grows.
// Bad: God component
function Dashboard() {
// 1000+ lines of code
// Multiple responsibilities
// Hard to test
}
// Good: Composed components
function Dashboard() {
return (
<DashboardLayout>
<MetricsPanel />
<RecentActivity />
<QuickActions />
</DashboardLayout>
);
}
Scalable Code Organization
Feature-Based Structure
src/
├── features/
│ ├── authentication/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ ├── types/
│ │ └── index.ts
│ ├── products/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ ├── types/
│ │ └── index.ts
│ └── checkout/
│ ├── components/
│ ├── hooks/
│ ├── services/
│ ├── types/
│ └── index.ts
├── shared/
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ └── types/
└── app/
├── routes/
├── layout/
└── App.tsx
Benefits:
- Features are self-contained
- Easy to locate code
- Clear ownership
- Can be extracted to packages
Layer-Based Architecture
src/
├── presentation/ # UI components
│ ├── pages/
│ └── components/
├── domain/ # Business logic
│ ├── entities/
│ ├── use-cases/
│ └── services/
├── data/ # Data access
│ ├── repositories/
│ ├── api/
│ └── cache/
└── infrastructure/ # External concerns
├── logging/
├── analytics/
└── config/
State Management at Scale
Local vs Global State
// Bad: Everything in global state
const globalStore = {
user: {...},
products: [...],
modalOpen: false, // UI state shouldn't be global
hoverState: {...}, // Transient state shouldn't be global
formData: {...} // Form state shouldn't be global
};
// Good: Appropriate state placement
// Global state (shared across app)
const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
login: (credentials) => {...},
logout: () => {...}
}));
// Local state (component-specific)
function ProductCard() {
const [isHovered, setIsHovered] = useState(false);
const [quantity, setQuantity] = useState(1);
// ...
}
// Feature state (shared within feature)
const useProductsStore = create((set) => ({
products: [],
selectedCategory: null,
filters: {}
}));
State Colocation
// Bad: State far from usage
function App() {
const [modalOpen, setModalOpen] = useState(false);
const [selectedProduct, setSelectedProduct] = useState(null);
return (
<div>
<Header />
<ProductList
onProductSelect={setSelectedProduct}
onModalOpen={() => setModalOpen(true)}
/>
{/* Prop drilling */}
</div>
);
}
// Good: State close to usage
function ProductList() {
const [selectedProduct, setSelectedProduct] = useState(null);
const [modalOpen, setModalOpen] = useState(false);
return (
<>
<ProductGrid onProductClick={setSelectedProduct} />
{modalOpen && <ProductModal product={selectedProduct} />}
</>
);
}
Data Fetching at Scale
Caching and Deduplication
// Using React Query for scalable data fetching
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Configure caching strategy
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
retry: 3
}
}
});
// Fetch with automatic caching
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
// Automatically deduplicates requests
// Multiple components can call this without extra network calls
});
}
// Prefetch for better UX
function ProductsPage() {
const queryClient = useQueryClient();
useEffect(() => {
// Prefetch next page
queryClient.prefetchQuery({
queryKey: ['products', page + 1],
queryFn: () => fetchProducts(page + 1)
});
}, [page]);
return <ProductList />;
}
Pagination and Infinite Scroll
// Efficient pagination
function useProductsPaginated(page: number, pageSize: number) {
return useQuery({
queryKey: ['products', page, pageSize],
queryFn: () => fetchProducts({ page, pageSize }),
keepPreviousData: true // Prevent loading flicker
});
}
// Infinite scroll
import { useInfiniteQuery } from '@tanstack/react-query';
function useProductsInfinite() {
return useInfiniteQuery({
queryKey: ['products'],
queryFn: ({ pageParam = 0 }) => fetchProducts(pageParam),
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length : undefined;
}
});
}
function ProductList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useProductsInfinite();
return (
<>
{data?.pages.map(page =>
page.products.map(product => (
<ProductCard key={product.id} product={product} />
))
)}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</>
);
}
Performance Optimization Patterns
Virtualization for Large Lists
import { FixedSizeList } from 'react-window';
function LargeProductList({ products }) {
return (
<FixedSizeList
height={600}
itemCount={products.length}
itemSize={100}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<ProductCard product={products[index]} />
</div>
)}
</FixedSizeList>
);
}
Memoization
import { memo, useMemo, useCallback } from 'react';
// Memoize expensive components
const ProductCard = memo(({ product, onAddToCart }) => {
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => onAddToCart(product)}>Add to Cart</button>
</div>
);
}, (prevProps, nextProps) => {
// Custom comparison
return prevProps.product.id === nextProps.product.id &&
prevProps.product.price === nextProps.product.price;
});
// Memoize expensive calculations
function ProductList({ products, category }) {
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === category);
}, [products, category]);
const handleAddToCart = useCallback((product) => {
// Add to cart logic
}, []);
return filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
));
}
Code Splitting Strategies
// Route-based splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));
// Component-based splitting
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function AnalyticsPage() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
// Library splitting (load on demand)
async function exportToExcel(data) {
const XLSX = await import('xlsx');
const worksheet = XLSX.utils.json_to_sheet(data);
// ...
}
Bundle Optimization
Analyzing Bundle Size
# Using webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
# Add to webpack config
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
Tree Shaking
// Bad: Imports entire library
import _ from 'lodash';
const result = _.debounce(fn, 300);
// Good: Import only what you need
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// Even better: Use modern alternatives
import { debounce } from 'lodash-es'; // ES modules version
Dynamic Imports
// Conditional loading
async function loadFeature(featureName: string) {
if (featureName === 'advanced-charts') {
const { AdvancedCharts } = await import('./features/AdvancedCharts');
return AdvancedCharts;
}
}
// User role-based loading
function AdminPanel() {
const { isAdmin } = useAuth();
if (!isAdmin) return null;
const AdminTools = lazy(() => import('./AdminTools'));
return (
<Suspense fallback={<div>Loading admin tools...</div>}>
<AdminTools />
</Suspense>
);
}
Part 2: Responsive Design
Mobile-First Approach
/* Mobile-first: Start with mobile styles */
.product-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
}
/* Tablet: 768px and up */
@media (min-width: 768px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
padding: 1.5rem;
}
}
/* Desktop: 1024px and up */
@media (min-width: 1024px) {
.product-grid {
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
padding: 2rem;
}
}
/* Large desktop: 1440px and up */
@media (min-width: 1440px) {
.product-grid {
grid-template-columns: repeat(4, 1fr);
max-width: 1400px;
margin: 0 auto;
}
}
Responsive Breakpoints
// Define consistent breakpoints
export const breakpoints = {
xs: '0px',
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px'
};
// Use in styled components
import styled from 'styled-components';
const Container = styled.div`
padding: 1rem;
@media (min-width: ${breakpoints.md}) {
padding: 2rem;
}
@media (min-width: ${breakpoints.lg}) {
padding: 3rem;
max-width: 1200px;
margin: 0 auto;
}
`;
// Or use CSS custom properties
:root {
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
}
Responsive Hooks
// useMediaQuery hook
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => setMatches(media.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [matches, query]);
return matches;
}
// Usage
function ProductGrid() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
const columns = isMobile ? 1 : isTablet ? 2 : 4;
return <Grid columns={columns}>{/* ... */}</Grid>;
}
// useBreakpoint hook
function useBreakpoint() {
const [breakpoint, setBreakpoint] = useState('md');
useEffect(() => {
const updateBreakpoint = () => {
const width = window.innerWidth;
if (width < 640) setBreakpoint('xs');
else if (width < 768) setBreakpoint('sm');
else if (width < 1024) setBreakpoint('md');
else if (width < 1280) setBreakpoint('lg');
else setBreakpoint('xl');
};
updateBreakpoint();
window.addEventListener('resize', updateBreakpoint);
return () => window.removeEventListener('resize', updateBreakpoint);
}, []);
return breakpoint;
}
// Usage
function Navigation() {
const breakpoint = useBreakpoint();
return breakpoint === 'xs' || breakpoint === 'sm'
? <MobileNav />
: <DesktopNav />;
}
Responsive Images
// Picture element for art direction
function ResponsiveHero() {
return (
<picture>
<source
media="(min-width: 1024px)"
srcSet="/images/hero-desktop.jpg"
/>
<source
media="(min-width: 768px)"
srcSet="/images/hero-tablet.jpg"
/>
<img
src="/images/hero-mobile.jpg"
alt="Hero banner"
loading="lazy"
/>
</picture>
);
}
// Responsive background images
.hero {
background-image: url('/images/hero-mobile.jpg');
background-size: cover;
background-position: center;
}
@media (min-width: 768px) {
.hero {
background-image: url('/images/hero-tablet.jpg');
}
}
@media (min-width: 1024px) {
.hero {
background-image: url('/images/hero-desktop.jpg');
}
}
// Srcset for resolution switching
<img
src="/images/product-800.jpg"
srcSet="
/images/product-400.jpg 400w,
/images/product-800.jpg 800w,
/images/product-1200.jpg 1200w
"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
alt="Product"
loading="lazy"
/>
Responsive Typography
/* Fluid typography using clamp() */
h1 {
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1.2;
}
h2 {
font-size: clamp(1.5rem, 4vw, 3rem);
}
p {
font-size: clamp(1rem, 2vw, 1.125rem);
line-height: 1.6;
}
/* Responsive spacing */
.container {
padding: clamp(1rem, 3vw, 3rem);
margin-bottom: clamp(2rem, 5vw, 5rem);
}
/* CSS custom properties for responsive values */
:root {
--font-size-base: 16px;
--spacing-unit: 8px;
}
@media (min-width: 768px) {
:root {
--font-size-base: 18px;
--spacing-unit: 12px;
}
}
@media (min-width: 1024px) {
:root {
--font-size-base: 20px;
--spacing-unit: 16px;
}
}
Container Queries
/* New: Container queries (experimental) */
.card-container {
container-type: inline-size;
container-name: card;
}
.card {
display: flex;
flex-direction: column;
}
/* Adjust layout based on container width, not viewport */
@container card (min-width: 400px) {
.card {
flex-direction: row;
}
.card-image {
width: 40%;
}
.card-content {
width: 60%;
}
}
Responsive Utilities
// Responsive utility system
const responsive = {
mobile: (...styles) => `
@media (max-width: 767px) {
${styles.join('\n')}
}
`,
tablet: (...styles) => `
@media (min-width: 768px) and (max-width: 1023px) {
${styles.join('\n')}
}
`,
desktop: (...styles) => `
@media (min-width: 1024px) {
${styles.join('\n')}
}
`
};
// Usage with styled-components
const Button = styled.button`
padding: 0.5rem 1rem;
font-size: 0.875rem;
${responsive.tablet`
padding: 0.75rem 1.5rem;
font-size: 1rem;
`}
${responsive.desktop`
padding: 1rem 2rem;
font-size: 1.125rem;
`}
`;
Best Practices
1. Performance Budget
// Define performance budgets
const budgets = {
maxBundleSize: 250 * 1024, // 250KB
maxInitialLoad: 3000, // 3 seconds
maxTimeToInteractive: 5000, // 5 seconds
maxImageSize: 500 * 1024 // 500KB
};
// Monitor in CI/CD
if (bundleSize > budgets.maxBundleSize) {
throw new Error(`Bundle size ${bundleSize} exceeds budget ${budgets.maxBundleSize}`);
}
2. Accessibility
// Ensure responsive design is accessible
function ResponsiveNav() {
const isMobile = useMediaQuery('(max-width: 768px)');
return isMobile ? (
<button
aria-label="Open navigation menu"
aria-expanded={menuOpen}
onClick={toggleMenu}
>
<MenuIcon />
</button>
) : (
<nav aria-label="Main navigation">
<NavLinks />
</nav>
);
}
3. Testing
// Test responsive behavior
import { render } from '@testing-library/react';
import { useMediaQuery } from './hooks/useMediaQuery';
// Mock window.matchMedia
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: query === '(max-width: 768px)',
media: query,
addEventListener: jest.fn(),
removeEventListener: jest.fn()
}))
});
});
test('renders mobile view on small screens', () => {
const { getByTestId } = render(<ResponsiveComponent />);
expect(getByTestId('mobile-view')).toBeInTheDocument();
});