There is no silver bullet in Software Engineering.
A good front-end application should have the following basic characteristics:
- High performance and good user experience (UX = User Experience)
- Simplicity and ease of maintenance resulting in high efficiency (DX = Developer Experience)
To achieve these goals, here are some experiences and principles summarized by @Arno during his daily development that emphasize the importance of thoughtful design, effective component decomposition, and the strategic use of performance optimization techniques.
Overall Design Principles
-
Design First, Then Implement. Start with macro design before implementation.
- Carefully choose foundational libraries and engineering architecture based on business requirements. Remember to evaluate the long-term viability and community support of these libraries to ensure they align with your project's goals and can adapt to future changes.
Less is more
because it helps to reduce complexity, making the codebase easier to maintain and understand while minimizing potential issues that may arise from over-reliance on numerous dependencies. - Consider use the right state design pattern, using appropriate frameworks and libraries to decompose application states into maintainable parts.
- Consider the decomposition of UI components, for example:
- View components (components with application-level states), usually tied with
router
level and are responsible for rendering the overall layout and structure of the application, ensuring a seamless user experience as users navigate through different routes. - Business components (foundational components with business characteristics, such as an internal contact selection component), encapsulate specific business logic and functionality, allowing for greater reusability and maintainability across different parts of the application.
- Basic components (general-purpose components without business characteristics, such as
AntD
and Material-UI, which provide reusable UI elements that can be easily integrated into various applications without being tied to specific business logic.
- View components (components with application-level states), usually tied with
- Focus on the design of data
immutability
, which can have remarkable effects in specific scenarios especially when it comes to optimizing performance by preventing unintended side effects and ensuring predictable state management throughout the application lifecycle. - Especially consider the "orthogonality" between components, meaning the separation of responsibilities between React components, extracting them into different view components for easier system maintenance and independence, which also facilitates testing.
- Business UI Scenario: rendering business UI elements
- Data Fetching Scenario: data fetching and UI state presentation
- Global State Scenario: global state listening and management
- Data persistence Scenario: components responsible for data I/O persistence, etc.
- ...
- Carefully choose foundational libraries and engineering architecture based on business requirements. Remember to evaluate the long-term viability and community support of these libraries to ensure they align with your project's goals and can adapt to future changes.
-
Pay appropriate attention to the "Evolutionary Design" of the current front-end application, considering the impact of external business and technological changes on the current architecture and future response strategies.
-
Configure unit testing for foundational business libraries, especially for foundational units that need to be used across different businesses, ensuring quality, and establish a DevOps testing mechanism, including unit tests, integration tests, or E2E tests. Use AI to automate the testing process, allowing for quicker feedback loops and enhanced accuracy in identifying potential issues before they reach production. → AI Driven FrontEnd Development
-
Develop scalable systems and adhere to design methods such as
IoC
(Inversion of Control) andDI
(Dependency Injection), and even introduce microkernel architecture; specific references can be consulted.- Use the proper module-loader or module-isolation mechanism to ensure that components remain decoupled and can be independently developed, tested, and deployed, which enhances the overall maintainability and scalability of the application. → 📦 Ways of Organize Web Modules in Browser
-
When facing complex business logic in systems, consistently use the object-oriented (OO) programming paradigm for structural design and functional implementation, enforcing the use of TypeScript as the programming language.
High Performance Principles
- Follow the performance optimization design specifications of the React framework. To avoid issues with duplicate rendering, refer to React ReRender BP, and extract some important principles:
- Understand the triggers for re-rendering: parent node re-rendering, node state changes, Context value changes, and Hook changes.
- 🚫 Do not create component classes in the render function (this will re-render the entire child component tree every time).
- ✅ Try to pass state down to "leaf" nodes as much as possible, minimizing intermediate states or related states.
- ✅ Pass children as props.
- ✅ Pass Component classes as props.
- ✅ Use React.memo to prevent updates from propagating downward.
- ✅ Correctly use useMemo() and useCallback() to cache computationally expensive values and functions.
- ✅ Use keys as unique identifiers (for lists).
- ✅ Use the useMemo mechanism for Context values.
- ✅ When injecting Context, separate data and API methods.
- ✅ Differentiate Context functions; avoid a single Context.Provider to rule them all.
- ✅ Use Context Selectors to update views as needed.
- Understand the triggers for re-rendering: parent node re-rendering, node state changes, Context value changes, and Hook changes.
- Decompose view components (FunctionComponent) as much as possible; avoid combining them into a large function, as using
useState()
within a function will cause the entire node and subtree Render function to execute, affecting performance. - Consider caching strategies at every level and keep them in mind. Additionally, use libraries that support caching appropriately to implement reasonable caching in high-frequency scenarios.
- Always enable the RenderHighlight feature in React DevTools to visually detect the actual rendering situation of the DOM. If duplicate rendering or imprecise rendering areas (redundant rendering) are found, immediately perform targeted optimizations.
- Pay attention to package size; use mechanisms like TreeShaking to eliminate redundant dependencies and be cautious when introducing external dependencies.
Stability Principles
- Use mature, high-quality open-source libraries, but be sure to use version locks, similar to
yarn.lock
mechanism to avoid potential breaking changes that could disrupt the stability of your application during updates. - When using Hooks, pay special attention to prevent circular dependencies; handle Hooks dependencies with great care. Using the linter can help identify potential issues early in the development process, ensuring a more stable and reliable application.
- Be mindful of
XSS
protection; non-trusted content needs to be escaped before output.
Maintainability Principles
- Unify the
code writing style
and writing principles within the team. ReadWrite Clean Code
or similar book to ensure that all team members adhere to best practices and maintain consistency throughout the codebase. Use kinds of linters to automatically enforce coding standards and catch potential errors early in the development process, fostering a collaborative environment where quality code is prioritized. - Unify the code organization style within the team: directory organization, project organization (MonoRepo), etc. For large projects, it is reasonable to decompose and allow technical and business autonomy; oligarchic applications are not conducive to large team operations.
- Reasonably use design patterns and design principles within the team to improve program structure and behavior organization, facilitating effective communication to form a consensus and unify the design language.
- Strictly adhere to the basic principle of separating views and logic (this is easy to say but often not followed).
- Use layered design thinking to manage application state reasonably.
- Control file length; often, the root of complexity comes from not segmenting and separating code in a lazy manner, leading to lengthy files. 500 lines may be the limit.
- For extracting React-type components, refer to the discussion on complexity in the frontend "component" design principles which provides feasible design principles.
Engineering Principles
-
Use AI Code Copilot to streamline the coding process, enhance productivity, and reduce the likelihood of errors by providing intelligent code suggestions and automating repetitive tasks.
-
Utilize good coding tools to enhance programming efficiency, such as
VSCode
and its powerful plugin ecosystem. -
Choose mature code build tools; for server-side rendering, use Next.js; for client-side SPAs, choose Vite; for static
SSG
, consider Astro, etc. Additionally, consider usingTurborepo
for MonoRepo project management. -
Maintain unified React Boilerplates within the team and provide a progressive upgrade mechanism that incorporates design principles and BP.
-
For complex web systems that require frequent maintenance by multiple teams, consider introducing architecture to enhance engineering collaboration efficiency and code isolation while maintaining SPA-level experiences.
-
Use new engineering technologies moderately without affecting stability, such as
vite
,esbuild
, orswc
to improve development efficiency. -
Focus on the efficiency of interface coordination, using GraphQL or some interface generation and mocking tools, as well as automatically generating
*.d.ts
data model definition files. Use OpenAPI Spec to Typescript API Client to ensure seamless integration between front-end and back-end services, facilitating smoother data exchanges and reducing the potential for errors in type definitions.
FullStack with Server
For full-stack experience guide, Arno write a Next.js
powered architecture guide to effectively integrate server-side rendering with API routes, ensuring seamless data fetching and dynamic content delivery while maintaining optimal performance and scalability. You can learn from 🪐 Next.js Full Stack App Architecture Guide.