Skip to content

Front-end Engineering and Application Design

The core issue in front-end engineering is not ‘which framework to choose,’ but how to organize the interface, state, data, and user interactions into a maintainable system. Pages start from the browser rendering model, then layer components, routing, requests, caching, build, and deployment on top. Frameworks will change, but layering and boundaries will not.

Native DOM can of course be used to write applications, but as the state increases, manually synchronizing data and the interface can easily get out of control:

User operation → modify data → update DOM
↑ │
└── Other operations ┘

The same set of data may be displayed in multiple locations, and network responses and local operations may be interleaved. The framework mainly provides three things:

  1. Declarative rendering: Describes “what the interface should look like” rather than modifying the DOM step by step.
  2. Componentization: Encapsulates structure, style, and behavior into bounded units.
  3. State-to-view synchronization: After the state changes, recalculates and renders in a controlled way.

React, Vue, Svelte, Solid, and others are implemented differently, but all need to handle components, props, state, lifecycle, events, and rendering scheduling.

Components can be understood as functions that receive input and return a description of the interface:

type ButtonProps = {
label: string;
disabled?: boolean;
onClick: () => void;
};
function Button({ label, disabled = false, onClick }: ButtonProps) {
return (
<button disabled={disabled} onClick={onClick}>
{label}
</button>
);
}

Props are inputs passed from the parent, whereas state is data within the component that can change. When the boundary between the two is unclear, the component can end up with a lot of synchronous logic.

Good component boundaries usually satisfy:

  • Clear inputs and outputs.
  • Do not directly rely on the external environment, or pass dependencies through parameters.
  • Only responsible for one interpretable interface duty.
  • Can be tested independently without needing to launch the entire application.
  • Styles and accessibility are handled on the same layer, without leaving behavior for the caller to guess.

Components are not necessarily better the smaller they are. Splitting a button into seven files, if they always change together, actually increases navigation cost. Splitting should be based on responsibilities, reuse, and frequency of change, not line count.

3. Rendering Model: From Declaration to Screen

Section titled “3. Rendering Model: From Declaration to Screen”

Different frameworks update differently:

  • Virtual DOM first constructs a lightweight interface tree, then compares it with the previous tree, and finally applies the differences.
  • Fine-grained reactivity tracks which views read which states, and only updates relevant parts when the state changes.
  • Compile-time updates analyze the template at build time and compile the update logic into more direct code.

All these methods aim to reduce unnecessary work. The real factors affecting performance are: update frequency, how many components are involved in each update, whether synchronous layout is triggered, and whether the state is placed too high in the hierarchy.

Form inputs can be controlled by the application state or maintained by the DOM itself. Controlled inputs are convenient for validation and linkage, but each input may trigger re-rendering; uncontrolled inputs are closer to native forms and are suitable for simple or high-performance scenarios. The choice depends on whether immediate synchronization is needed, not framework preference.

3.2 Server-side Rendering vs Client-side Rendering

Section titled “3.2 Server-side Rendering vs Client-side Rendering”
Mode Where HTML Comes From Advantages Considerations
CSR Generated by browser executing scripts Rich interactions, easy deployment First screen depends on JS, SEO and weak network experience may suffer
SSR Generated on the server per request First screen content loads faster Server load, streaming rendering, state synchronization
SSG Static pages generated at build time Cheap, stable, suitable for content sites Content updates require rebuilding
Hybrid Rendering Page or component chooses separately Balances static and dynamic Cache, boundaries, and debugging are more complex

After SSR returns HTML, the client still needs to “hydrate,” attaching events and state. Hydration mismatches can cause flickering, double rendering, or event anomalies. SSR is not necessarily faster by default; it adds server and cache complexity in exchange for faster first screen and better indexability.

Routing maps URLs to pages and states. Single-page applications often use the History API to change addresses without reloading the entire document; server-side routing returns different responses based on paths.

Routing design should answer:

  • Which states should go into the URL to be recoverable through refresh, sharing, and back navigation?
  • Which pages require login, permissions, or data prefetching?
  • Which requests should be canceled and which caches can be reused when switching pages?
  • How should 404, empty state, and error state be displayed?
  • Do deep links and browser back/forward work correctly?
/orders Order List
/orders/:id Order Details
The tab in the details of /orders/:id?tab=items

Filter criteria, pagination, and tabs are usually suitable to be placed in the URL; temporary pop-ups and input focus do not necessarily need this. Nested routes can express layout reuse, but do not nest the entire application into dozens of confusing layers.

Splitting code by routes or large features can reduce the download size of the first screen. Lazy loading is not a free lunch: users will have to wait the first time they visit a page, and you need to design preloading strategies, loading placeholders, and retry mechanisms for failures.

5. State Management: Classify First, Then Choose Tools

Section titled “5. State Management: Classify First, Then Choose Tools”

“Global state” is often overused. Actual state can be divided into several categories:

State Type Example Suitable Place
Local UI state Popup toggle, input value Inside the component
Cross-component UI state Theme, sidebar, selected item Locally shared context or small store
URL state Page, filter, pagination, tabs Route
Server data User, order, list Request layer cache and query management
Form state Field value, error, dirty state Form library or within the component
Derived state Total price, filtered results, whether empty Calculated from source state, do not store repeatedly

The most dangerous thing is storing the same fact in multiple places: for example, user information exists in the global store, local state, and URL simultaneously, and only one place is updated. Prioritize a single source of truth, then decide on caching and derived data.

Server data can expire, may be modified by other clients, and requires pagination, retries, and invalidation strategies. It is not the same kind of issue as “whether a popup is open.” At the request layer, you usually need:

  • Deduplication and cache keys for requests.
  • Loading, success, empty data, and error states.
  • Expiration time and refetching.
  • Local invalidation after user actions.
  • Cancellation, timeout, and race condition handling.

When two requests return out of order, old requests may overwrite new results. Use request IDs, cancellation signals, or version comparisons to ensure only the current result updates the interface.

Do not just split all code by “components / services / utils.” As the project grows, one business feature may contain pages, components, requests, types, and tests simultaneously. A more durable structure is organized by domain or feature:

src/
app/
router.ts
providers.tsx
features/
orders/
api.ts
model.ts
OrderList.tsx
OrderDetail.tsx
OrderList.test.tsx
users/
...
shared/
ui/
lib/
config/

Principle:

  • Internal business functions can be highly cohesive, exposing only a few external entry points.
  • Only put code that is truly cross-functional and reusable, to avoid turning it into a junkroom.
  • API types, validation, and error structures should be close to the caller.
  • The page is responsible for composition, not for all business details.
  • Try to keep dependencies one-way; low-level tools should not import pages back.

7. Build Tools: Transpiling, Packaging, and Artifacts

Section titled “7. Build Tools: Transpiling, Packaging, and Artifacts”

Browsers only need HTML, CSS, JavaScript, and resource files that can be executed. Modern frontend source code may include TypeScript, JSX, CSS Modules, image imports, and new syntax, so a build chain is required.

Stage What to do
Transpile TypeScript/JSX/new syntax into code that the target browser can execute
Dependency Analysis Find out who depends on whom among the modules
Packaging/Unpacking Merge or split products, control loading size
Resource Handling Images, Fonts, CSS, Hashed Filenames
Optimization Compression, Tree Shaking, Code Splitting
Generated Product Output files and manifest that can be directly deployed

Vite uses the browser’s native modules and fast transformations during development, while production builds are usually handed over to a more complete bundling process; Webpack’s loaders are responsible for transforming files, and plugins are responsible for extending the build lifecycle. The tools are different, but both need to answer the same questions: What is the entry point, where is the output, how are environment variables injected, and how does the cache become invalid.

Variables injected into the frontend during build will end up in the browser output and anyone can see them. API keys, database passwords, and server tokens should not be placed in frontend environment variables. Public configurations can be injected, but secrets must remain on the server or controlled proxy.

Source Map maps the compressed code back to the source code, making debugging easier. Publicly exposing production Source Maps may reveal the source code structure and internal paths; they can be uploaded only to error monitoring services or access can be restricted.

8. Performance: Measure First, Then Optimize

Section titled “8. Performance: Measure First, Then Optimize”

Common indicators:

  • LCP (Largest Contentful Paint): When the largest content element becomes visible, reflecting the loading experience.
  • INP (Interaction to Next Paint): How long it takes for the interface to respond after an interaction.
  • CLS (Cumulative Layout Shift): Whether the page content suddenly shifts.

Optimization broken down by stages:

Stage Problem Common Methods
Loading Too many resources, too slow, blocking rendering Compression, bundling, lazy loading, caching, CDN
Rendering DOM too large, complex layout, images without fixed size Virtual list, placeholder sizes, reduce nesting
Interaction Long tasks and repeated calculations on the main thread Split tasks, use Worker, cache derived values
Animation Trigger layout or repaint on each frame transform/opacity, requestAnimationFrame

Images need to have width, height, or aspect ratio set to avoid layout collapse after loading; fonts need to consider font-display, subsetting, and layout differences. Performance metrics should be observed according to device, network, and region, and not just measured on the development machine.

Rendering thousands of records all into the DOM will consume memory and layout time. A virtual list only renders items near the viewport, but it increases the complexity of scrolling, variable heights, and focus management. When the amount of data is not large, a regular list is simpler and more reliable.

Method Direction Suitable
Polling Client requests at regular intervals Simple, low-frequency, tolerant of delay
Long Polling The server holds the request until there is data Good compatibility, but connection management is complex
SSE Server-to-client one-way push Notifications, progress, streaming text
WebSocket Bidirectional long connection Chat, collaboration, games
WebRTC Peer-to-peer audio, video, and data Real-time media, requires signaling and network negotiation

Real-time connections must handle reconnection after disconnection, duplicate messages, ordering, heartbeat, authentication expiration, and page hiding. SSE/WebSocket only handle transmission; the business logic still requires message IDs, idempotency, and recovery strategies.

Streaming results should be able to distinguish between ‘partially successful’ and ‘fully successful’. When the user closes the page or cancels an operation, the client should send a cancel signal, and the server should also stop meaningless background work.

10. Accessibility and Internationalization

Section titled “10. Accessibility and Internationalization”

Accessibility is not an extra feature, but the correct use of the web platform:

  • Use semantic elements to represent titles, buttons, links, and forms.
  • All interactions can be completed using the keyboard, and there is a clear focus style.
  • Images have appropriate alternative text, and decorative images can be marked as empty.
  • Form errors provide textual explanations as well, not relying solely on color.
  • When the popup opens, manage the focus, and after it closes, return the focus to the triggering element.
  • When dynamic content is updated, inform assistive technologies using an appropriate live region.

ARIA can supplement information when native semantics are insufficient, but ‘adding a lot of aria-*’ does not equal accessibility. Incorrect ARIA is worse than none.

Internationalization includes translation, date/number formats, plural rules, text direction, length variations, and time zones. Use the Intl API to handle localized formats, and do not hard-code all language rules. Layouts should allow long German words, right-to-left Arabic, and Chinese line breaks without spaces.

  • Do not insert unescaped user content into innerHTML.
  • Whitelist restrictions for URLs, redirects, and third-party scripts.
  • Sensitive operations are authenticated on the server side; hiding buttons on the frontend does not count as permissions.
  • Cookie sets HttpOnly, Secure, and SameSite according to the scenario.
  • Use CSP, dependency auditing, and least privilege to reduce XSS and supply chain risks.
  • Third-party components and analytics scripts may read page data; the source and necessity must be evaluated.

Frontend testing is roughly divided into three layers:

  • Unit Testing: formatting, calculations, state transitions, validation functions.
  • Component/Integration Testing: user actions, forms, request mocks, error states.
  • End-to-End Testing: follow key paths in a real browser, login, payment, permissions.

Don’t just assert DOM class names; behaviors visible to users are more worthwhile to test. Visual regression can find layout issues, but screenshot differences are noisy, so stable pages and key breakpoints should be chosen.

Pre-launch checks:

  1. Whether route refresh and 404 fallback are compatible with the deployment platform.
  2. Whether static resources use the correct base path and cache headers.
  3. Whether environment variables only contain public values.
  4. Whether error monitoring can pinpoint the version and Source Map.
  5. Whether the first screen, slow network, mobile, and keyboard operations have been verified.
  6. Whether cached Service Workers from old versions can safely update.
  • Are component inputs and outputs clear, and is repeated state avoided?
  • Do routes cover refresh, deep links, permissions, and error pages?
  • Does server-side data handle caching, expiration, cancellation, and race conditions?
  • Is the project structure organized according to change boundaries rather than just technical-layer directories?
  • Are build artifacts reproducible, and are all front-end environment variables treated as public?
  • Is performance driven by real metrics, and are the main thread and layout checked?
  • Are real-time connections handling disconnections, duplicates, ordering, and cancellation?
  • Are semantics, keyboard, focus, contrast, and alternative text checked?
  • Are security boundaries on the server-side and not just hidden on the front end?
  • Are normal, empty, loading, error, and insufficient permission states tested?