《川剧变脸 × React 状态管理?我在浏览器里跑了个端侧大模型》

从零到一:在浏览器中运行 DeepSeek-R1,深度解析端侧大模型与 React + WebGPU 实战

前言

你是否曾经遇到过这样的困扰:调用大模型 API 价格高昂,企业数据通过网络传输存在安全隐患,或者想在离线环境下使用 AI 能力却无从下手?其实有一种更优雅的方案------端侧模型 ,也就是把 LLM 直接部署在用户的设备上。今天,我将带你一步步搭建一个运行在浏览器里的 DeepSeek-R1 蒸馏版推理 Demo,只依靠 React + TailwindCSS + Transformers.js,全程无需服务器,数据绝对安全。

在这次实战中,代码中的每一行注释都藏着重要的知识点。我们不仅会实现功能,还会逐条解读这些注释背后的技术原理:从函数式组件的"数据驱动"思想 ,到 Hooks 的状态与副作用 ,再到 TailwindCSS 的原子类哲学,以及 WebGPU 的硬件加速检测。完整代码和踩坑经验全部奉上,让我们一起探索"AI + 前端"的无限可能。

🚨 剧透预警 :这只是整个项目的第一部分!目前我们完成了界面骨架和状态体系,下一篇文章将带你加上底部的模型下载进度条,让加载过程一目了然。记得关注,别错过续集哦~


一、为什么需要端侧大模型?

在传统的大模型应用架构中,我们通常会通过 API 调用远程的 LLM 服务,比如 OpenAI 或 DeepSeek 的云端接口。这种方式存在两个致命问题:

  • 成本高:按 Token 计费,长期使用对企业或个人都是一笔不小的开销。
  • 不安全:所有上下文(context)数据都会随请求发送到服务器,敏感信息有泄露风险。

端侧模型(On-device Model)将 LLM 直接部署在用户的设备上,比如手机、车载系统,甚至是浏览器里。它的核心优势:

  • 数据隐私有保障:所有推理都在本地完成,数据不出设备。
  • 离线可用:一次下载模型后,即使断网也能正常使用。
  • 低成本:无需按量付费,尤其适合高频简单任务。
  • 低延迟:跳过了网络传输,响应速度更快。

当然,端侧模型对硬件有一定要求,通常我们使用开源小参数模型 (例如 1.5B 量级),它们经过蒸馏和优化后,足以完成翻译、摘要、简单对话等任务。而在浏览器端运行,则可以借助 WebGPU 硬件加速,实现随时随地下载、随时使用的极致轻量化体验。

📌 小贴士:想要在本地玩转端侧模型,可以先用 Ollama 体验一键部署开源模型的快乐。它支持 Windows/Linux/macOS,一行命令就能把 Llama、Qwen 等模型跑在本地。但今天我们更进一步,让模型直接跑在浏览器标签页里!


二、技术选型:React + TypeScript + TailwindCSS + WebGPU

现代大型 AI 项目的前端,几乎绕不开 React。和 Vue 相比,React 的上手门槛略高,但凭借其灵活的函数式编程范式和庞大的生态,成为了大模型应用的首选。特别是函数式组件 + Hooks的组合,让数据和 UI 的关系变得异常清晰------你只需要定义状态,React 就会自动帮你更新界面。

对比 Vue 的 SFC(template + script + style 三明治结构),React 组件本质是一个返回 JSX 的函数,JS 逻辑和 HTML 结构天然融合在一起,CSS 则通过独立的样式文件或 TailwindCSS 这种原子化工具注入。

技术栈一览

  • React 18 + TypeScript :强类型约束 + 函数式组件 + Hooks(useState, useEffect 等)
  • Vite:极速构建工具,原生支持 JSX/TSX
  • TailwindCSS:原子化 CSS 框架,几乎不再需要手写 CSS
  • ESLint:代码风格统一,大公司协作必备
  • Transformers.js + ONNX Runtime Web:在浏览器中加载和运行蒸馏版大模型
  • WebGPU:调用本地 GPU 硬件加速推理

三、项目初始化:从一行命令开始

打开终端,用 Vite 模板一把梭创建 React + TypeScript 项目:

bash 复制代码
npm init vite@latest deepseek-webgpu -- --template react-ts
cd deepseek-webgpu
npm install

生成的项目已经配置好了 ESLint,保证代码风格一致------这对于团队项目或开源项目尤为重要。比如它会强制你使用单引号还是双引号,缩进是 2 空格还是 4 空格,告别混乱的代码格式。


四、引入 TailwindCSS:告别传统 CSS 的低效

传统 CSS 需要定义选择器、编写属性值,然后还要小心处理优先级和样式冲突。这就像是用二进制直接编程,底效且容易出错。 TailwindCSS 带来了一种"原子化"的思维:它提供了成千上万个语义化的类名(比如 flex, text-center, bg-white),你只需在 HTML 标签上组合这些类名,就能快速构建出复杂的界面。

安装并配置 TailwindCSS(基于 Vite 插件):

bash 复制代码
npm install -D tailwindcss @tailwindcss/vite

修改 vite.config.ts

typescript 复制代码
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
})

在入口 CSS 文件(src/index.css)顶部添加:

css 复制代码
@import 'tailwindcss';

现在你可以在组件里直接使用类似 className="flex flex-col items-center" 的原子类了。注意,JSX 中我们用 className 而非 class,因为 class 是 JavaScript 的关键字(用于声明类),所以 React 选择了 className 替代,这算是 JSX 语法的一个小细节。


五、React 函数组件:搭积木式的 UI 构建

React 将界面拆分成一个个独立的"积木"------组件。每个组件其实就是一个返回 JSX 的函数,函数内部可以写任何 JavaScript 逻辑,最后返回一段看起来像 HTML 的 JSX 代码。

理解 JSX:JSX 是 JavaScript 与 XML 的结合,它让我们可以在 JS 文件中直接书写 HTML 标签,编译后会被转成原生的 DOM 操作。这是 React 最骄傲的特性之一,让 UI 表达变得直观而高效。

看看入口文件 src/main.tsx 是如何挂载组件的:

tsx 复制代码
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

<App /> 就是我们自己的顶层组件,它会被渲染到页面 ID 为 root 的 div 中。整个应用就像一棵由组件构成的树,层层嵌套。


六、核心组件 App.tsx 逐步拆解(注释深度解析版)

下面就是我们的主角------App.tsx。我将带你结合上课笔记,逐行剖析其中的关键知识点。特别注意代码中的注释,它们比代码本身更重要------每一条注释都是对设计思想、API 原理和工程实践的提炼。

1. 理解现代前端框架的本质

现代前端框架的三大核心概念:组件化响应式数据绑定

组件化 :无论是 React 还是 Vue,都将界面拆分成独立、可复用的组件。每个组件都像一个黑盒子,内部封装了自身的结构(HTML)、样式(CSS)和交互逻辑(JS)。在 React 中,这个"黑盒子"就是一个函数;在 Vue 中,它是一个 .vue 单文件。这种封装性让我们可以像搭积木一样组合出复杂页面,极大地提升了代码的可维护性和复用性

响应式 :这是现代框架与 jQuery 时代最本质的区别。我们不再需要手动查找 DOM 节点并修改其属性,而是声明式地描述"当数据为 X 时,UI 应该长什么样"。框架会自动追踪数据的变化,并高效地更新对应的 DOM 节点。React 通过 useState 触发重新渲染,Vue 则通过 refreactive 的 Proxy 拦截实现。响应式机制让开发者可以专注于数据和业务逻辑,而不必陷入繁琐的 DOM 操作。

数据绑定 :数据绑定是连接界面与数据的桥梁。在 JSX 中,我们使用 {} 进行单向数据绑定:数据流向视图。例如 <p>{count}</p>,当 count 变化时,<p> 的内容会自动更新。Vue 则支持双向绑定(v-model),但 React 推崇单向数据流,这样做更易于追踪状态变化,避免数据混乱。数据驱动视图的理念,不需要去做 DOM 编程 -> 数据状态(响应式,修改状态,界面就会跟着变)。

"函数封装特性"则强调了 React 的组件就是一个函数,它具备了函数的天然优势:接收参数、返回结果、可组合、可复用。这种以函数为最小单元的抽象方式,让 UI 的构建过程变得像编写普通的 JavaScript 逻辑一样自然。

2. 引入 Hooks:函数式思想的精髓

tsx 复制代码
import {
  useState , // react 函数式思想 hooks     每个函数都有一个hooks 钩子状态,以use开头
  useEffect , // 生命周期钩子函数 组件挂载时执行
} from 'react'
  • useState 注释解读 :React 的函数组件本身是无状态的,但通过 useState 这个 Hook,我们可以为组件"挂载"响应式状态。就像给函数装了一个钩子,让它能够记住数据。凡是名字以 use 开头的函数,都是 React 的 Hook,它们只能在组件函数的最顶层调用。
  • useEffect 注释解读useEffect 被称为副作用 Hook,它可以模拟类组件中的生命周期方法。当组件挂载(首次插入 DOM)时、状态更新时、甚至销毁时,都可以触发里面的逻辑。第二行的注释"组件挂载时执行"只是一个常见用法,实际上它的触发时机由依赖数组决定。

3. 函数组件的结构注释:职责分明的代码骨架

一个函数组件的标准写法:

  • 头上挂 Hooks :组件顶部调用各种 useStateuseEffect 等,声明状态和副作用。
  • 中间写逻辑:处理数据、事件、条件判断等。
  • 最后吐 JSX:返回一段 JSX 模板,描述 UI 的结构。

React 组件就是这样一个纯函数,输入数据,输出界面,干净且可预测。

4. 状态设计的哲学:UI = f(state)

tsx 复制代码
  const [status, setStatus] = useState<string | null>(null); // 响应式数据状态

这段注释极其重要,它揭示了 React 的核心理念------数据驱动视图

  • 传统 DOM 编程 vs 响应式 :以前我们需要手动选择 DOM 元素,修改其 innerTextclassName。而现在,我们只需要定义好状态(如 status),并描述每种状态下界面应该长什么样,React 会自动完成 DOM 更新。
  • 状态枚举null(尚未开始)、loading(加载中)、ready(就绪)、error(失败)......这些状态就像川剧变脸的面具,数据一变,界面瞬间切换,用户看到的就是当前时刻的"快照"。
  • 类型约束useState<string | null>(null) 通过泛型指定了状态只能是字符串或 null,TypeScript 会帮我们严格把关。

5. 错误状态与加载进度:让反馈更细腻

tsx 复制代码
  const [ error, setError] = useState<Error | null>(null); // 错误对象数据状态
  const [ loadingMessage, setLoadingMessage ] = useState(""); // 加载中状态
  const [ progressItem, setProgressItem ] = useState([{
    file:'model.onnx',
    progress:0,
    total:66666666,
  }]); // 进度状态
  • error 存储一个 JavaScript Error 对象,可以从中提取 messagestack,实现精确的错误提示。
  • loadingMessage 提供动态加载文字,比如"正在下载模型文件...",缓解等待焦虑。
  • progressItem 跟踪模型文件(如 ONNX 格式的 model.onnx)的下载进度,progresstotal 分别为已下载和总字节数。这里敲黑板:progressItem 就是为下篇文章的下载进度条预留的! 目前界面中还没有渲染它,但状态已经就位,下一篇我们会在页面底部加一条酷炫的缓存进度条,让加载过程一目了然。

6. WebGPU 能力检测:双重否定的技巧

tsx 复制代码
  // 浏览器 导航栏 是否支持 WebGPU
  // 现代浏览器的重要特性
  // ! 取反 navigator.gpu 不支持的时候 undefined
  // !! 再取反,一定苦于变成 true | false
  // 双重否定等于肯定
  const IS_WEBGPU_AVAILABLE = !!navigator.gpu;

这段注释生动地解释了双重否定操作:

  • navigator.gpu 是 WebGPU 的入口对象,如果浏览器不支持,它为 undefined
  • 一个 ! 将其转换为布尔值并取反:undefinedtrue对象false
  • 两个 ! 再取反一次,就得到了原始的布尔值:undefinedfalse对象true
  • 所以 !! 等价于 Boolean() 强制类型转换,常用于将"可能为假值"的变量转为确切的 true/false。这里得到 true 表示浏览器支持 WebGPU,可以继续后续的硬件加速推理。

7. useEffect 的挂载与副作用

tsx 复制代码
  // useEffect 用,status 状态变化时执行
  // 组件的生命周期 副作用
  // 副作用 组件挂载后,附带做什么

  useEffect(() => {
    console.log('组件已经挂载完成')
    // setTimeout(() => {
    //    setStatus('ready')
    // }, 2000)
  }, [status])
  • 副作用(Side Effect) 指的是那些与组件渲染无关但又必须执行的操作,比如数据请求、订阅、手动修改 DOM 等。这里的 console.log 就是一个简单的副作用。
  • 依赖数组 [status] :表示只有当 status 的值发生变化时,useEffect 内的函数才会重新执行。如果依赖数组为空 [],则只在组件首次挂载时执行一次,类似 componentDidMount
  • 注释中提到用 setTimeout 模拟状态变化,从 null 变成 ready,这正是我们后续加载模型时需要实现的核心流程。

8. 组件函数的执行与状态变更

tsx 复制代码
  console.log('组件函数执行')
  // js 脚本 数据逻辑交互
  // count 数据状态
  // 修改count setCount
  const [count, setCount] = useState(0) // 响应式状态 ref

这里特意引入了一个未在 UI 中使用的 count 状态,目的是演示 React 的重渲染机制。每当通过 setCount 改变状态时,整个 App 函数会被重新调用,控制台会再次打印"组件函数执行"。但注意,count 的值会被 React 保留并递增,因为 useState 底层通过链表维护了组件的所有状态。这个实验证明:函数组件虽然是普通的 JavaScript 函数,但在 React 的运行环境中拥有了"记忆"能力。

9. JSX 中的条件渲染与布局注释

tsx 复制代码
  return (
    // 原子类,组合一下 
    IS_WEBGPU_AVAILABLE ? (
      <div className="flex flex-col h-screen mx-auto items-center justify-center text-gray-800 bg-white">
  • 条件渲染IS_WEBGPU_AVAILABLE ? (...) : (...) 三元运算符是最直接的条件渲染方式,根据 WebGPU 支持情况展示不同的完整界面。这种写法保证了每个分支的 UI 描述都是完整的,易于阅读。

  • 原子类组合flex flex-col h-screen mx-auto items-center justify-center 这一串类名实现了一个灵活的垂直居中布局:

    • flex 开启 Flex 布局
    • flex-col 主轴纵向
    • h-screen 高度占满整个视口
    • mx-auto 水平方向自动外边距(居中)
    • items-center 交叉轴居中(横向居中)
    • justify-center 主轴居中(纵向居中)
  • 注意 JSX 的注释格式 :在 JSX 中,注释需要用 {/* 注释内容 */} 包裹,而不能使用普通的 ///* */,因为 JSX 最终会被编译为 JavaScript 调用,普通注释会被当作文本节点处理。

10. 标题区域注释:模型信息与社区链接

tsx 复制代码
        {/* 标题区域 */}
        <div className="flex flex-col items-center mb-4 max-w-[400px] text-center">
          {/* 蒸馏的是Qwen */}
          <h1 className="text-4xl font-bold mb-1">DeepSeek-R1 WebGPU</h1>
          <h2 className="font-semibold">
            A next generation reasoning model that runs locally in your browser with WebGPU acceleration.
          </h2>
        </div>
  • 蒸馏(Distillation):注释指出模型是蒸馏自 Qwen 架构。知识蒸馏是一种模型压缩技术,它用一个大的"教师模型"教导小的"学生模型",使得小模型在保持接近大模型精度的同时,参数量大幅减少,非常适合端侧部署。

11. 模型介绍区域:开源生态的黄金三角

tsx 复制代码
            <a
              href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX"
              target="_blank"
              rel="noreferrer"
              className="font-medium underline"
            >
              {/* DeepSeek-R1 的 15 亿参数量蒸馏版,用 Qwen 架构,适合本地轻量推理。
                  蒸馏Qwen Reasoning 推理
                  HuggingFace 抱抱脸 全球最大开源模型社区
              */}
              DeepSeek-R1-Distill-Qwen-1.5B
            </a>
            , a 1.5B parameter reasoning LLM optimized for in-browser
            inference. Everything runs entirely in your browser with{" "}

            {
              // transformers 是huggingface 提供的js 库,用于加载和推理模型
            }
            <a
              href="https://huggingface.co/docs/transformers.js"
              target="_blank"
              rel="noreferrer"
              className="underline"
            >
              🤗&nbsp;Transformers.js
            </a>{" "}
            {/* Open Neural Network Exchange */}
            and ONNX Runtime Web, meaning no data is sent to a server. Once
            loaded, it can even be used offline. The source code for the demo
            is available on{" "}

这一段注释极富信息量,堪称整个 Demo 的"生态说明书":

  • DeepSeek-R1-Distill-Qwen-1.5B:15 亿参数的推理模型,专为本地轻量推理优化,特别擅长复杂的推理任务(Reasoning)。
  • HuggingFace 🤗 :全球最大的开源模型社区,提供了模型托管、版本管理、推理库等一系列工具,被开发者亲切地称为"抱抱脸"。我们使用的模型就托管在它的 onnx-community 组织下。
  • Transformers.js:HuggingFace 官方出品的 JavaScript 推理库,让 Transformer 模型能在浏览器中运行,无需后端。它内部会调用 ONNX Runtime Web 或者直接使用 WebGPU。
  • ONNX Runtime Web:开放神经网络交换格式的运行时,支持 WebGPU 后端,能进一步加速模型推理。ONNX 是一种通用的模型存储格式,可以让训练好的模型在不同框架和硬件上运行。
  • 隐私承诺:所有计算都在浏览器本地完成,数据不发送到任何服务器,甚至可以离线使用。这是端侧模型最大的魅力所在。

12. 错误界面:响应式错误提示

tsx 复制代码
          {/* 报错界面状态,响应式 */}
          {
          error && (
            <div className="text-red-500 mb-2 text-center">
              <p className="mb-1">
                Unable to load model due to the following error:
              </p>
              <p className="text-sm">{error.message}</p>
            </div>
          )}
  • 利用 error && (...) 实现条件渲染:只有当 error 不为 null 时,错误提示卡片才会显示。因为 error 是一个响应式状态,一旦它被设置,组件会立刻重新渲染,用户会即时看到错误信息。这种"即变即显"的体验,是传统 DOM 操作难以优雅实现的。

七、模型加载流程:Transformers.js 与 ONNX Runtime 联动

useEffect 或其他生命周期中,我们实际会调用 Transformers.js 的管道 API 来下载模型和分词器等资源。同时利用 setProgressItem 更新下载进度,setStatus 切换状态。一旦模型就绪,status 变为 ready,用户即可开始对话。

在当前的 UI 阶段,我们还没有实现真正的推理逻辑,但所有状态和界面骨架已经完整搭建。后续只需要在点击按钮或输入文本时,调用模型管道并更新 loadingMessage 或对话历史即可。


八、总结与展望

通过这个项目,我们串联起了端侧大模型的核心概念、React 函数式组件与 Hooks、TailwindCSS 原子化样式,以及 WebGPU 硬件加速的实战运用。但更重要的是,我们逐条品味了代码中的注释------它们是思想的凝结,是知识的注解。

你学到了什么?

  • 端侧模型的价值与适用场景,数据安全永远第一。
  • React 函数组件 = Hooks(状态+副作用)+ 逻辑 + JSX,简单而强大。
  • 注释是一种工程素养:好的注释能解释"为什么这么做",而不只是"做了什么"。
  • 原子类与响应式思维,让 UI 开发变得像写自然语言一样流畅。
  • WebGPU 为浏览器端的高性能 AI 推理打开了新大门。

未来,端侧模型将在物联网、自动驾驶、工业边缘计算等领域大放异彩。而浏览器作为最普适的终端,必将成为 AI 能力分发的重要阵地。希望这篇从零开始的实战文章,能帮你推开这扇门,用代码和注释一起构建更智能的应用。


九、下集预告:进度条来了!🚀

到此为止,我们的 DeepSeek-R1 WebGPU 项目已经搭建好了完整的 UI 骨架和状态体系。界面上能看到标题、模型介绍链接,也能根据 WebGPU 支持情况和错误状态做出响应------唯独还差底部的模型下载进度条

别急,这恰好是我们下篇文章的重头戏!到时候我们会:

  • 在页面底部加入一条优雅的进度条组件
  • 利用 progressItem 状态实时更新下载百分比
  • 配合 loadingMessage 显示动态提示文字
  • 让整个加载体验从"干等"变成"可视化"的享受

嘿,感谢你读到这里! 🎉 如果这篇文章让你对端侧 AI 和 React 有了新的火花,别吝啬你的点赞和收藏,顺手转发给同样在折腾大模型前端的小伙伴吧~ 有什么脑洞大开的端侧 AI 玩法,或者踩过的坑,评论区就是你的舞台 ,咱们一起交流、一起进步!别忘了点个关注,下篇进度条实战已经在路上了,不见不散!👋

相关推荐
winfredzhang1 小时前
用 wxPython + ECharts + 阿里矢量地图,做一个可离线兜底的上海雨量看板
前端·javascript·echarts
李福春1 小时前
SpecCoding + Harness:把 Vibe Coding 从「灵感」钉成「可交付」
架构
索西引擎1 小时前
【React】Immer.js 在现代 Redux 生态中的角色:不可变性保障的工程化实现与开发体验优化
前端·javascript·react.js
只一1 小时前
端侧AI实战第二章:React组件工程化 + 事件系统 + 可复用进度条(WebGPU模型加载底座)
前端·react.js
小小猪的春天1 小时前
AI Code Review 例外决策框架:手动忽略警告之前,先回答4个问题
后端·架构
大卫陈1 小时前
微信小程序虚拟支付实战:从「支付能力被限制」到沙箱调通的全过程
前端·后端
武子康1 小时前
vLLM 0.25.1:服务没有报错,为什么仍会生成垃圾 Token(5 级正确性门禁 + 自动回滚条件)
前端·人工智能·后端
码云骑士1 小时前
71-Agent记忆系统-短期记忆-长期记忆-向量知识库三层架构
python·架构
jyp201211071 小时前
Vue3 Diff 算法
前端·vue.js