产品战略专家梁宁确认出席AICon北京站,分享AI时代下的商业逻辑与产品需求 了解详情
写点什么

React 18 深度解析:应用性能提升之道

作者 | Lydia Hallie

  • 2023-08-29
    北京
  • 本文字数:9428 字

    阅读完需:约 31 分钟

React 18深度解析:应用性能提升之道

React 18 引入的并发功能,从根本上改变了 React 应用程序的渲染方式。本文将带大家一同探讨这些最新功能的具体作用,特别是如何提高应用程序性能。


首先,我们先从长任务基础知识说起,聊聊其中的性能测量思路。


主线程与长任务


当我们在浏览器中运行 JavaScript 时,JS 引擎会在单线程环境下执行代码内容,而该环境通常称为主线程。除了执行 JS 代码之外,主线程还负责处理其他任务,包括管理用户交互(如鼠标单击和键盘输入)、处理网络事件、计时器、更新动画并管理浏览器的重排和重绘等。



主线程负责逐一处理各项任务


在处理一项任务时,其余任务均处于等待状态。虽然浏览器在执行小规模任务时能提供丝滑无缝的用户体验,但面对更长的任务时则可能出现问题。由于任务耗时较长,其他任务可能一直被悬置和等待。一般来说,任何运行时间超过 50 毫秒的任务均被定义为“长任务”。



这里的 50 毫秒标准基于以下事实:设备必须每 16 毫秒生成一个新帧(相当于 60 fps)才能保持流畅的视觉体验。然而,设备在此期间还需要执行其他任务,例如响应用户输入和执行 JavaScript 代码。


所以 50 毫秒基准测试能保证设备为帧渲染和其他任务保留必要的资源,始终提供额外的约 33.33 毫秒时间执行其他任务。感兴趣的朋友可以参阅这篇博文:https://web.dev/rail/#response-process-events-in-under-50ms


其中借由 RAIL 模型探讨了关于 50 毫秒基准测试的更多信息。




为了保持最佳性能,尽量控制长任务的数量显然非常重要。在测量网站性能时,我们一般通过两项指标来了解长耗时任务对应用程序性能的影响:总阻塞时间,以及下次绘制的交互。


总阻塞时间(TBT)是用于衡量首次内容绘制(FCP)和交互时间(TTI)之间间隔时长的重要指标。总阻塞时间代表着所有执行时间超过 50 毫秒的任务的总和,它们往往是对用户体验影响最大的因素。



上图中的总阻塞时间为 45 毫秒,因为我们有两个任务在首次交互时间(TTI)之前花费超过 50 毫秒,而超出这项阈值的部分分别是 30 毫秒和 15 毫秒。所以总阻塞时间就是这些值的累加:30 毫秒 +15 毫秒 =45 毫秒。


下次绘制的交互(INP)则是 Core Web Vitals 提出的新指标,测量的是用户在第一次与页面交互(例如单击某个按钮)到交互结果在屏幕上显示出来之间的间隔,即下次绘制。该指标对于包含大量用户交互元素的页面特别重要,例如电子商务网站或者社交媒体平台。它的衡量方式,是将用户当前访问期间所有 INP 的测量值累加起来,再计算出最差得分。



上图中的下次绘制的交互时间为 250 毫秒,这也是测量期间得到的最高视觉延迟。


要了解 React 18 的更新如何针对这些指标进行优化、从而改善用户体验,我们首先需要明确 React 以往版本的工作原理。


以往 React 中的渲染机制


React 中的视觉更新主要分为两个阶段:渲染阶段与提交阶段。这里的渲染阶段属于纯计算阶段,期间 React 元素与现有 DOM 进行协调(即比较)。此阶段需要创建新的 React 元素树,也被称为“虚拟 DOM”,它本质上就是 DOM 在轻量级内存中的表示形式。


在渲染阶段,React 会计算当前 DOM 和新 React 组件树之间的差异,并准备好必要的更新。



渲染阶段之后则是提交阶段。在此阶段,React 会将渲染阶段计算出的更新应用于实际 DOM。具体过程包括创建、更新和删除 DOM 节点,从而映射新的 React 组件树。




在传统的同步渲染中,React 会给组件树中的所有元素赋予相同的优先级。在对组件树进行渲染时,无论是在初始渲染还是在状态更新时,React 都会在单一不间断任务中持续渲染该树,之后将结果提交给 DOM 以直观更新屏幕上显示的组件。



同步渲染属于是“全有或全无”的操作,它会保证已经开始渲染的组件必定能够完成。而根据组件的具体复杂性,渲染阶段可能要持续一段时间才能完成。在此期间,主线程将始终被阻塞,因此用户与 UI 间的交互将失去响应,直到 React 完成渲染并将结果提交至 DOM。




以下演示就展现了这样的情况。我们设置了一个文本输入字段,外加一份巨大的城市列表,这里要根据文本输入的当前值进行过滤。在同步渲染中,React 会在每次键盘输入时重新渲染 CityList 组件。因为列表当中包含数以万计的城市,所以计算负担相当沉重,导致键盘输入和显示文本响应之间出现相当明显的视觉反馈延迟。


App.js:


import React, { useState } from "react";import CityList from "./CityList"; export default function SearchCities() {  const [text, setText] = useState("Am");    return (          <main>                <h1>Traditional Rendering</h1>                <input type="text" onChange={(e) => setText(e.target.value) }   />                <CityList searchQuery={text} />          </main>       );};
复制代码


CityList.js:


import cities from "cities-list";import React, { useEffect, useState } from "react";const citiesList = Object.keys(cities); const CityList = React.memo(({ searchQuery }) => {  const [filteredCities, setCities] = useState([]);   useEffect(() => {    if (!searchQuery) return;     setCities(() =>      citiesList.filter((x) =>         x.toLowerCase().startsWith(searchQuery.toLowerCase())      )    );   }, [searchQuery]);   return (     <ul>       {filteredCities.map((city) => (         <li key={city}>           {city}        </li>       ))}    </ul>    )}); export default CityList;
复制代码


index.js:


import { StrictMode } from "react";import ReactDOM from "react-dom";import App from "./App";import "./styles.css"; const rootElement = document.getElementById("root"); ReactDOM.render(<StrictMode><App /></StrictMode>,  rootElement);
复制代码


style.css:


* {  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,    Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;}:root {  --foreground-rgb: 0, 0, 0;  --background-rgb: 244, 244, 245;  --border-rgb: 228, 228, 231;}@media (prefers-color-scheme: dark) {  :root {    --foreground-rgb: 255, 255, 255;    --background-rgb: 0, 0, 0;    --border-rgb: 39, 39, 42;    --input-background-rgb: 28, 28, 28;  }}body {  color: rgb(var(--foreground-rgb));  background: rgb(var(--background-rgb));}h1 {  margin-bottom: 2em;  font-size: 1.5em;}input {  border: 1px solid rgb(var(--border-rgb));  border-radius: 3px;  padding: 1em 2em;  font-size: 1.1em;  background-color: rgb(var(--input-background-rgb));  color: rgb(var(--foreground-rgb));  outline: none;  min-width: 70vw;}code {  font-family: Menlo;  font-size: 90%;  background: rgb(var(--border-rgb));  padding: 0.3em 0.5em;  border-radius: 3px;}main {  padding: 1em 3em;  display: flex;  flex-direction: column;  align-items: center;}ul {  overflow: scroll;  padding: 0;  min-width: 70vw;}li {  list-style-type: none;  padding: 1em;  border-bottom: 1px solid rgb(var(--border-rgb));}
复制代码


如果大家使用的是 MacBook 这样高端计算设备,可能需要限制 CPU 4x 来模拟低端设备性能。大家可以在 Devtools > Performance > ⚙️ > CPU 中找到这项设置。


在查看性能选项卡时,可以发现每次键盘输入都会产生长任务,这明显有违优化原则。



标有红角的任务就是“长任务”。请注意,这里的总阻塞时间为 4425.40 毫秒。


在这种情况下,React 开发人员经常会使用 debounce 等第三方库来做延迟渲染,但 React 本身并没有内置的解决方案。




React 18 引入了幕后运行的新并发渲染器。该渲染器为我们提供了一些将某些渲染任务标记为非紧急的办法。



当渲染低优先级组件(粉色)时,React 会返回主线程以检查是否存在更重要的任务。


在这种情况下,React 每 5 毫秒就会返回一次主线程,看看有没有更重要的任务等待处理。这可能是用户输入,也可能是渲染另一个对当前用户体验更重要的 React 组件状态更新。通过不断返回主线程,React 就能让此类渲染获得非阻塞保障、凭借更高的重要性得到优先处理。



并发渲染器并不再为每次渲染执行一项不可中断的任务,而是在低优先级组件的(重新)渲染期间以 5 毫秒为间隔,定期为主线程提供控制权。


此外,并发渲染器还能在后台“同时”渲染组件树的多个版本,且无需立即提交结果。


相较于全有或全无的同步渲染计算,并发渲染器允许 React 暂停和恢复对一个或多个组件树的渲染,从而实现最佳用户体验。



React 会根据用户交互暂时叫停当前渲染,强制要求主线程优先渲染另一更新。


使用并发功能,React 能够根据外部事件(如用户交互)暂停和恢复组件渲染。当用户开始与 COmponentTwo 交互时,React 会暂停当前渲染、优先渲染 ComponentTwo,之后再恢复渲染 ComponentOne。我们将在暂停部分具体讨论这部分内容。


过渡(Transitions)


我们可以使用 userTransition 钩子提供的 startTransition 函数,将某些更新标记为非紧急。这是一项强大的新功能,允许我们将某些状态更新标记为“transitions”,表明它们与视觉变化相关,如果继续以同步渲染方式处理则可能会破坏用户体验。


通过在 startTransition 中打包状态更新,我们可以要求 React 推迟或中断当前渲染,腾出手来优先处理更重要的任务,从而保证当前用户界面的可交互性。


import { useTransition } from "react"; function Button() {  const [isPending, startTransition] = useTransition();   return (    <button       onClick={() => {        urgentUpdate();        startTransition(() => {          nonUrgentUpdate()        })      }}    >...</button>  )}
复制代码


在过渡开始时,并发渲染器会在后台准备新树。一旦完成渲染,它会将结果保留在内存内,直到 React 调度程序可以更有效地更新 DOM 以反映新状态。具体时间点可能是在浏览器空闲,而且没有待处理的高优先级任务(例如用户交互)的情况下。



对于演示中的 CityList 用例来说,过渡机制的表现堪称完美。我们可以将状态拆分成两个值,并将 searchQuery 的状态更新打包在 startTransition 当中,而不再在每次键盘输入时直接将更新传递给 searchQuery 参数(这会导致每次键盘输入都触发同步渲染)。


这种方式相当于告知 React,某些状态更新可能会导致视觉变化,进而对用户造成干扰。因此 React 应该尽量保持当前 UI 的可交互性,同时在后台准备新状态、但暂时不立即提交。


index.js:


import { StrictMode } from "react";import ReactDOM from "react-dom/client";import App from "./App";import "./styles.css"; const rootElement = document.getElementById("root");const root = ReactDOM.createRoot(rootElement); root.render(<StrictMode><App /></StrictMode>);
复制代码


App.js:


import React, { useState, useTransition } from "react";import CityList from "./CityList"; export default function SearchCities() {  const [text, setText] = useState("Am");  const [searchQuery, setSearchQuery] = useState(text);  const [isPending, startTransition] = useTransition();    return (          <main>                <h1><code>startTransition</code></h1>                <input                type="text"               value={text}              onChange={(e) => {                 setText(e.target.value)                 startTransition(() => {                    setSearchQuery(e.target.value)                 })             }}  />                <CityList searchQuery={searchQuery} />          </main>       );};
复制代码


CityList.js:


import cities from "cities-list";import React, { useEffect, useState } from "react"; const citiesList = Object.keys(cities); const CityList = React.memo(({ searchQuery }) => {  const [filteredCities, setCities] = useState([]);   useEffect(() => {    if (!searchQuery) return;     setCities(() =>      citiesList.filter((x) =>         x.toLowerCase().startsWith(searchQuery.toLowerCase())      )    );   }, [searchQuery]);   return (     <ul>       {filteredCities.map((city) => (         <li key={city}>{city}</li>       ))}    </ul>    )}); export default CityList;
复制代码


style.css:


* {  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,    Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;}:root {  --foreground-rgb: 0, 0, 0;  --background-rgb: 244, 244, 245;  --border-rgb: 228, 228, 231;}@media (prefers-color-scheme: dark) {  :root {    --foreground-rgb: 255, 255, 255;    --background-rgb: 0, 0, 0;    --border-rgb: 39, 39, 42;    --input-background-rgb: 28, 28, 28;  }}body {  color: rgb(var(--foreground-rgb));  background: rgb(var(--background-rgb));}h1 {  margin-bottom: 2em;  font-size: 1.5em;}input {  border: 1px solid rgb(var(--border-rgb));  border-radius: 3px;  padding: 1em 2em;  font-size: 1.1em;  background-color: rgb(var(--input-background-rgb));  color: rgb(var(--foreground-rgb));  outline: none;  min-width: 70vw;}code {  font-family: Menlo;  font-size: 90%;  background: rgb(var(--border-rgb));  padding: 0.3em 0.5em;  border-radius: 3px;}main {  padding: 1em 3em;  display: flex;  flex-direction: column;  align-items: center;}ul {  overflow: scroll;  padding: 0;  min-width: 70vw;}li {  list-style-type: none;  padding: 1em;  border-bottom: 1px solid rgb(var(--border-rgb));}
复制代码


现在,当我们在输入字段处键入内容时,用户的输入感受始终流畅,每次键入间没有任何视觉延迟。这是因为 text 状态始终保持同步更新,并供输入字段作为 value 使用。


在后台,React 开始在每次键入时渲染新树。但这时其执行的已经不再是全有或者全无的同步任务,React 开始在内存中准备组件树的新版本,而当前 UI(显示「旧」状态)则继续响应用户的每一步后续输入。


下面来看性能选项卡。与不使用 transition 的实现相比,将状态更新打包进 startTransition 能显著缩降低长任务数量和总阻塞时间。



性能选项卡显示,长任务数量和总阻塞时间均有显著降低。


过渡也是 React 渲染模型中的根本性转变之一,令 React 能够同时渲染多个版本的 UI,并管理不同任务间的优先级差异。这将带来更流畅、响应更灵敏的用户体验,这一点在处理高频更新或 CPU 密集型渲染任务时效果尤其突出。


React Server Components


React Server Components 是 React 18 中的一项实验性功能,但已经为框架应用做好了准备。有了这个前提,下面我们开始深入探讨 Next.js。


在传统上,React 为我们的应用程序提供几种主要的渲染方式。我们可以完全在客户端侧渲染所有内容(即客户端渲染),也可以在服务器上将组件树渲染为 HTML,再使用 JavaScript 捆绑包将该静态 HTML 发送至客户端,从而在客户端上填充组件(即服务器侧渲染)。



这两种方法都基于这样一个事实:同步的 React 渲染器需要使用附带的 JavaScript 捆绑包在客户端重建组件树,即使该组件树已经在服务器上可用也不例外。




React Server Components 允许 React 将实际的序列化组件树发送至客户端。客户端 React 渲染器能够识别这种格式,并使用它高效重建 React 组件树,期间无需发送 HTML 文件或者 JavaScript 捆绑包。



我们可以将 react-server-dom-webpack/server 的 renderToPipeableStream 方法与 react-dom/client 的 createRoot 方法结合起来,使用这种新的渲染模式。


// server/index.jsimport App from '../src/App.js'app.get('/rsc', async function(req, res) {    const {pipe} = renderToPipeableStream(React.createElement(App));  return pipe(res);}); ---// src/index.jsimport { createRoot } from 'react-dom/client';import { createFromFetch } from 'react-server-dom-webpack/client';export function Index() {  ...  return createFromFetch(fetch('/rsc'));}const root = createRoot(document.getElementById('root'));root.render(<Index />);
复制代码


⚠️ 这里是后文中 CodeSandbox 演示的极度简化(请注意!)示例。


点击此处查看完整的 CodeSandbox 演示(https://codesandbox.io/p/sandbox/cocky-minsky-m7sgfx)。在下一节中,我们将介绍更为详尽的示例。




在默认情况下,React 不会对 React 服务器组件进行水合。这些组件不应使用任何客户端交互性(例如 window 对象),也不会使用 useState 或 useEffect 等钩子。


要将组件及其导入添加至发送到客户端的 JavaScript 捆绑包中以使其具有交互性,我们可以使用文件开头的“use client”捆绑器指令。这是告知捆绑器将此组件及其导入添加到客户端捆绑包内,并告知 React 对树客户端进行水合以添加交互性。此类组件,就被称为 Client Components。



注意:框架的具体实现可能有所区别。例如,Next.js 会在服务器上将客户端组件预渲染为 HTML,类似于传统 SSR 方法。但在默认情况下,Client Components 的渲染更类似于 CSR 方法。


在使用 Client Components 时,开发人员需要对捆绑包大小进行优化。具体优化方式包括以下几种:


  • 确保只有指令组件的最末叶节点处定义“use client”指令。这可能需要进行某些组件解耦。

  • 将组件树作为 props 传递,而不是直接导入。这样 React 可以将 children 渲染为 React Server Components,因此不必将其添加至客户端捆绑包内。


Suspense


React 18 中另一个重要新并发功能是 Suspense。虽然 Suspense 并非全新(最初发布于 React 16),最初用于通过 React.lazy 进行代码分割,但 React 18 将 Suspense 扩展到了数据获取。


使用 Suspense,我们可以延迟组件渲染,直到满足某些条件(例如从远程源加载数据)再恢复。同时,我们可以渲染一个后备组件,指示该组件仍然加载。


通过对加载状态做声明性定义,我们减少了对渲染逻辑条件的需求。将 Suspense 与 React Server Components 结合使用,我们能够直接访问服务器侧数据源(例如数据库或文件系统),而不需要借助单独的 API 端点。


async function BlogPosts() {  const posts = await db.posts.findAll();  return '...';} export default function Page() {  return (    <Suspense fallback={<Skeleton />}>      <BlogPosts />    </Suspense>  )}
复制代码


使用 React Server Components 与 Suspense 无缝协作,我们能够在组件仍在加载时定义加载状态。


Suspense 的真正力量,来自它与 React 并发功能的深度集成。当组件被挂起时,例如仍在等待数据加载,React 不会在组件真正收到数据前保持闲置。相反,它会暂停组件的渲染,并将其焦点转移至其他任务。



在此期间,我们可以告知 React 渲染一个后备 UI,以指示该组件仍在加载。一旦等待的数据可用,React 将以可中断的方式无缝恢复对先前挂起组件的渲染,效果与我们之前讨论的过渡(transition)一样。


React 还能根据用户交互重新调整各组件的优先级。例如,当用户与当前未渲染的挂起组件进行交互时,React 会挂起当前正进行的渲染,并优先处理用户正在与之交互的组件。



在准备就绪之后,React 会将其提交至 DOM,并恢复之前的渲染。这确保了用户交互的优先级,且 UI 将保持响应并根据用户输入保持随时最新状态。


Suspense 与 React Server Component 的可流式传输格式相结合,将使高优先级更新在准备好后立即发送至客户端,而无需等待低优先级渲染任务的完成。这样客户端能够更快开始处理数据,并保证将非阻塞方式到达的内容逐渐显示出来,从而提供更流畅的用户体验。


这种可中断的渲染机制与 Suspense 处理异步操作的能力相结合,带来更流畅、更加以用户为中心的体验。这在具有大量数据获取需求的复杂应用程序中尤其重要。


数据获取


除了渲染更新之外,React 18 还引入一个新的 API,用以有效获取数据并在内存中存储结果。


React 18 现在提供一个缓存函数,能够存储已打包的函数调用的结果。如果您在同一渲染通道中使用具有相同参数的同一函数,则 React 18 会使用内存内的值,无需再次执行该函数。


import { cache } from 'react' export const getUser = cache(async (id) => {  const user = await db.user.findUnique({ id })  return user;}) getUser(1)getUser(1) // Called within same render pass: returns memoized result.
复制代码


在 fetch 调用中,React 18 现在默认包含类似的缓存机制,而无需使用 cache。这有助于减少单个渲染通道中的网络请求数量,从而提高应用程序性能并降低 API 成本。


export const fetchPost = (id) => {  const res = await fetch(`https://.../posts/${id}`);  const data = await res.json();  return { post: data.post } } fetchPost(1)fetchPost(1) // Called within same render pass: returns memoized result.
复制代码


这些功能能够与 React Server Components 配合发挥重要作用。因为 React Server Components 无法访问 Context API,而缓存与获取的自动缓存行为允许从全局模块导出单个函数,并在整个应用程序范围内复用。



async function fetchBlogPost(id) {  const res = await fetch(`/api/posts/${id}`);  return res.json();}  async function BlogPostLayout() {  const post = await fetchBlogPost('123');  return '...'}async function BlogPostContent() {  const post = await fetchBlogPost('123'); // Returns memoized value  return '...'} export default function Page() {  return (    <BlogPostLayout>      <BlogPostContent />    </BlogPostLayout>  )}
复制代码


总结


总的来说,React 18 的各项新功能在诸多方面提高了应用程序的性能水平。


  • 使用 Concurrent React 并发功能,渲染过程可以暂停并稍后恢复,甚至将其弃用。这样即使是在执行大型渲染任务期间,UI 也能立即响应用户的输出。

  • Transitions API 允许在数据获取或屏幕内容变更期间,实现更平滑的过渡而不会阻止用户输入。

  • React Server Components 允许开发人员构建起可在服务器和客户端上运行的组件,将客户端应用程序的交互性与传统服务器渲染的高性能相结合,且无需借助水合机制。

  • 扩展后的 Suspense 功能允许先呈现应用程序中的某些部分,期间逐步显示其他可能需要更长时间获取的数据,从而提高加载性能。


原文链接:


https://vercel.com/blog/how-react-18-improves-application-performance


相关阅读:


React JS 广受业界认可,高级开发者年薪百万

从新 React 文档看未来 Web 的开发趋势

我被 React 劫持了,很痛苦又离不开

React 开发者们的 Solid.js 快速入门教程

2023-08-29 08:003992

评论

发布
暂无评论
发现更多内容

叹为观止!GitHub标星过万,腾讯技术官发布的“神仙文档”图解网络,简直是秋招福音

程序员 互联网 网络通信协议 计算机知识

手把手教你AspNetCore WebApi:入门

AI代笔

ASP.NET Core web api

看了这篇网络编程,就可以和面试官聊聊了

Simon郎

网络编程 websocket Java 分布式

「国庆」忆读书生涯

我是程序员小贱

美食 旅行

spring-boot-route(六)整合JApiDocs生成接口文档

Java旅途

Java Spring Boot

云服务器网站打开速度过慢,如何进行自检

德胜网络-阳

架构师训练营第四周作业

邓昀垚

极客大学架构师训练营

架构师训练营第 1 期 - 第 4 周 - 学习总结

wgl

金秋十月重磅技术文——网络编程大揭秘

Java架构师迁哥

编程 程序员

一个草根的日常杂碎(10月5日)

刘新吾

随笔杂谈 生活记录 社会百态

作者谈《阿里巴巴Java开发手册(规约)》背后的故事

Java架构师迁哥

makefile从入门到入门

MySQL从删库到跑路

c++ Linux 编译 makefile

阿里P8大牛爆肝的《Java核心技术总结》+《面试题总结》简直赞爆了

Java架构之路

Java 程序员 面试 编程语言 进阶

架构师训练营 Week4 系统架构 - 学习总结 架构演进

LeetCode题解:102. 二叉树的层序遍历,递归,JavaScript,详细注释

Lee Chen

大前端 LeetCode

4 个问题图解浏览器垃圾回收的过程

Java架构师迁哥

小伙伴想学Jenkins自动构建发布项目,我:安排上了!!

冰河

项目管理 jenkins 灰度发布 自动构建 及时发布

第一周-食堂就餐卡系统设计-UML设计

kawayi

架构训练营-week4-学习总结

于成龙

架构 作业 互联网架构 架构训练营

纸质书和书写的慢时代

boshi

随笔杂谈

中台: 54 天搞定中国百强企业的库存中心建设,而时间还能够再缩短至少一倍

日编一码

手把手教你锤面试官01——HashMap面试全攻略

慵懒的土拨鼠

面试 java基础

spring-boot-route(七)整合jdbcTemplate操作数据库

Java旅途

Java Spring Boot JDBC

Linux搭建C++开发调试环境

MySQL从删库到跑路

c++ Linux gdb 编译

Code Review怎么做

胖鱼2号

手把手教你AspNetCore WebApi:增删改查

AI代笔

ASP.NET Core web api EF Core

Chrome浏览器架构

曲迪

chrome 大前端 浏览器 专栏

我把这个贼好用的Excel导出工具开源了!!

冰河

Java Excel 冰河 mykit-excel

手把手教你AspNetCore WebApi:Swagger(Api文档)

AI代笔

ASP.NET Core swagger web api

《统计学习基础:数据挖掘、推理和预测》-斯坦福大学人工智能学科专用教材

计算机与AI

技术与思想:区块链的双重属性

CECBC

区块链 大数据

React 18深度解析:应用性能提升之道_架构/框架_InfoQ精选文章