前言
你还在调用第三方AI接口?按token扣费、用户输入数据全上传服务器、隐私风险拉满。
最近我完整落地一套浏览器本地跑DeepSeek-R1大模型项目,全程不用后端、不用API密钥,依靠WebGPU调用本地显卡推理,断网也能用。
整个项目覆盖前端全套核心技能: React+TS工程规范、Tailwind原子CSS、可复用业务组件封装、WebGPU兼容性判断、模型下载进度条、Hooks状态管理、JSX合成事件。

读完你能学会:
一、为什么选择WebGPU端侧模型?3大核心优势
市面上AI使用方案分三类,对比差距非常明显:
1. 云端API(OpenAI/DeepSeek官方接口)
- 缺点:按调用计费、用户prompt全部上传服务端,隐私泄露风险高;并发高时接口限流、响应延迟不可控
2. Ollama本地部署
- 缺点:需要用户手动安装客户端、下载模型,门槛高,无法开箱即用
3. WebGPU浏览器端侧模型(本项目方案)
✅ 零安装:打开网页自动下载量化小模型,开箱即用 ✅ 数据本地闭环:所有对话、推理仅在浏览器运行,不上传任何内容 ✅ 离线可用:模型下载完成后断网也能对话 ✅ 低成本:无接口费用,仅消耗用户本地显卡算力 ✅ 适配多端:PC浏览器、平板、高性能移动端均可运行
配套技术栈: React18 + TypeScript + TailwindCSS + Transformers.js + ONNX Runtime Web
二、项目基础技术前置知识点
2.1 React + TS 大型项目优势
AI相关前端项目优先选择React+TS,原因:
2.2 TailwindCSS 运行原理(告别手写CSS)
很多人只会复制Tailwind类名,底层原理一知半解:
2.3 JSX与React合成事件
JSX核心作用
React专属语法,支持JS内直接书写XML式HTML标签,编译后转为原生DOM操作,是React核心特性。
// JSX代码
<div className="box">文本</div>
// 编译后原生JS
React.createElement("div", {className: "box"}, "文本")
合成事件底层逻辑
- DOM0:行内onclick(耦合严重,不推荐)
- DOM2:addEventListener标准事件监听
2.4 React两种核心数据:Props & State
组件内数据分为两类,规范边界不能混淆:
三、实战1:封装通用Progress进度条组件(完整可运行代码)
项目需要展示模型分片下载进度,抽离独立复用组件,兼顾容错、字节格式化、空值兼容。
3.1 工具函数:字节单位格式化
模型文件动辄几百MB,封装方法自动转换B/kB/MB // Provarfunction formatBytes(size: number) { // size为0直接使用B单位 const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); return ( +(size / Math.pow(1024, i)).toFixed(2) * 1 + [“B”, “kB”, “MB”, “GB”, “TB”][i] ); }
### 3.2 完整进度条组件代码
核心亮点:
– 使用ES12空值合并赋值`percentage ??= 0`,兼容父组件不传进度的场景
– 动态宽度行内样式控制进度填充
– 自动判断是否传入文件总大小,展示容量信息
– Tailwind原子类完成全部样式,无独立css文件
```typescript
// Progress.ts
const Progress = ({ text, percentage, total }: {
text: string;
percentage?: number;
total?: number;
}) => {
// 容错:不传百分比默认赋值0
percentage ??= 0;
return (
<div className="w-full bg-gray-100 text-left rounded-lg overflow-hidden mb-0.5">
{/* 动态宽度控制进度 */}
<div
style={{ width: `${percentage}%` }}
className={`bg-blue-400 whitespace-nowrap px-1 text-sm`}
>
{text} {percentage.toFixed(2)}%
{/* 无总大小则不展示容量文本 */}
{isNaN(total) ? "" : ` of ${formatBytes(total)}`}
</div>
</div>
);
};
export default Progress;
3.3 组件踩坑提醒
四、实战2:WebGPU本地大模型完整页面App.tsx
页面实现功能:
// App.tsx
import { useState, useEffect } from 'react';
import Progress from '../components/Progress';
function App() {
// 输入框文本状态
const [input, setInput] = useState('');
// 模型状态:ready就绪 / loading下载中 / error报错
const [status, setStatus] = useState("ready");
// 错误信息存储
const [error, setError] = useState<null | string>(null);
// 下载提示文案
const [loadingMessage, setLoadingMessage] = useState("开始加载");
// 多模型文件下载进度数组
const [progressItems, setProgressItems] = useState<Array<{
text: string;
percentage?: number;
total?: number;
}>>([]);
// WebGPU兼容性判断
const IS_WEBGPU_AVALABLE = !!navigator.gpu;
// 发送对话回调
const onEnter = () => {
console.log('发送提问:', input);
// 后续可接入Transformers.js推理逻辑
};
// 组件挂载生命周期
useEffect(() => {
console.log('页面组件挂载完成');
}, []);
return (
IS_WEBGPU_AVALABLE ? (
<div className="flex flex-col h-screen mx-auto items-center justify-end text-gray-800 bg-white">
<div className="h-full overflow-auto flex justify-center items-center flex-col relative">
<div className="flex flex-col items-center mb-1 max-w-[400px] text-center">
<h1 className="text-4xl font-bold mb-1">DeepSeek–R1 WebGPU</h1>
<h2 className="font-semibold">
浏览器本地运行推理模型,无需后端、保护隐私
</h2>
</div>
<div className="flex flex-col items-center px-4">
<p className="max-w-[510px] mb-4">
加载 <a
href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX"
target="_blank"
rel="noreferrer"
className="font-medium underline"
>DeepSeek–R1 1.5B蒸馏轻量化模型</a>,基于Transformers.js+ONNX Web运行,所有数据仅保存在本地,支持离线对话。
</p>
{/* 错误提示区域 */}
{error && (
<div className="text-red-500 text-center mb-2">
<p className="mb-1">模型加载失败:</p>
<p className="text-sm">{error}</p>
</div>
)}
{/* 加载按钮,加载/报错状态禁用 */}
<button
className="border px-4 py-2 rounded-lg bg-blue-400 text-white hover:bg-blue-500 disabled:cursor-not-allowed select-none"
disabled={status !== "ready" || error !== null}
onClick={() => setStatus("loading")}
>
加载本地模型
</button>
</div>
</div>
{/* 模型下载进度列表 */}
{status === "loading" && (
<div className="w-full max-w-[500px] text-left mx-auto p-4 mt-auto">
<p className="text-center mb-1">{loadingMessage}</p>
{progressItems.map((item, i) => (
<Progress
key={i}
text={item.text}
percentage={item.percentage}
total={item.total}
/>
))}
</div>
)}
{/* 聊天输入框 */}
<div className="mt-2 border border-gray-300 rounded-lg w-[600px] max-w-[80%] max-h-[200px] mx-auto relative mb-3 flex">
<textarea
className="w-full px-3 py-4 rounded-lg bg-transparent border-none outline-hidden disabled:text-gray-400"
placeholder="输入你的问题…"
rows={1}
disabled={status !== "ready"}
value={input}
// 双向绑定模拟:onInput同步文本
onInput={(e) => {
const target = e.target as HTMLTextAreaElement;
setInput(target.value);
}}
// 回车发送,Shift+Enter换行
onKeyDown={(e) => {
if (input.length > 0 && e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onEnter();
}
}}
title={status === 'ready' ? '模型就绪,可以提问' : "请等待模型加载完成"}
/>
</div>
</div>
) : (
<div className="h-screen flex items-center justify-center text-xl text-gray-600">
当前浏览器不支持WebGPU,无法运行本地AI模型,请升级Chrome/Edge浏览器
</div>
)
);
}
export default App;
页面开发踩坑清单
五、端侧AI项目落地核心痛点解决方案
痛点1:用户浏览器不支持WebGPU
解决方案:
痛点2:大模型文件下载缓慢、重复下载
解决方案:
痛点3:模型下载过程页面卡死
解决方案: 使用Web Worker单独运行模型加载、推理逻辑,脱离主线程,UI操作不会阻塞
痛点4:进度数据状态混乱,多文件进度互相覆盖
解决方案: 使用数组存储每个文件独立进度对象,循环渲染独立进度条,每项数据隔离互不干扰
六、整体项目架构总结
1. 分层结构
- 页面层:App.tsx 负责整体布局、状态管理、交互逻辑
- 通用组件层:Progress.ts 可复用业务组件,纯展示接收Props
- 工具函数层:formatBytes 通用计算工具,解耦业务逻辑
2. 核心技术能力复盘
一、为什么选择WebGPU端侧模型?3大核心优势
市面上AI使用方案分三类,对比差距非常明显:
1. 云端API(OpenAI/DeepSeek官方接口)
- 缺点:按调用计费、用户prompt全部上传服务端,隐私泄露风险高;并发高时接口限流、响应延迟不可控
2. Ollama本地部署
- 缺点:需要用户手动安装客户端、下载模型,门槛高,无法开箱即用
3. WebGPU浏览器端侧模型(本项目方案)
✅ 零安装:打开网页自动下载量化小模型,开箱即用 ✅ 数据本地闭环:所有对话、推理仅在浏览器运行,不上传任何内容 ✅ 离线可用:模型下载完成后断网也能对话 ✅ 低成本:无接口费用,仅消耗用户本地显卡算力 ✅ 适配多端:PC浏览器、平板、高性能移动端均可运行
配套技术栈: React18 + TypeScript + TailwindCSS + Transformers.js + ONNX Runtime Web
二、项目基础技术前置知识点
2.1 React + TS 大型项目优势
AI相关前端项目优先选择React+TS,原因:
2.2 TailwindCSS 运行原理(告别手写CSS)
很多人只会复制Tailwind类名,底层原理一知半解:
2.3 JSX与React合成事件
JSX核心作用
React专属语法,支持JS内直接书写XML式HTML标签,编译后转为原生DOM操作,是React核心特性。
// JSX代码
<div className="box">文本</div>
// 编译后原生JS
React.createElement("div", {className: "box"}, "文本")
合成事件底层逻辑
- DOM0:行内onclick(耦合严重,不推荐)
- DOM2:addEventListener标准事件监听
2.4 React两种核心数据:Props & State
组件内数据分为两类,规范边界不能混淆:
三、实战1:封装通用Progress进度条组件(完整可运行代码)
项目需要展示模型分片下载进度,抽离独立复用组件,兼顾容错、字节格式化、空值兼容。
3.1 工具函数:字节单位格式化
模型文件动辄几百MB,封装方法自动转换B/kB/MB/GB/TB:
// Progress.ts
function formatBytes(size: number) {
// size为0直接使用B单位
const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return (
+(size / Math.pow(1024, i)).toFixed(2) * 1 +
["B", "kB", "MB", "GB", "TB"][i]
);
}
3.2 完整进度条组件代码
核心亮点:
- 使用ES12空值合并赋值percentage ??= 0,兼容父组件不传进度的场景
- 动态宽度行内样式控制进度填充
- 自动判断是否传入文件总大小,展示容量信息
- Tailwind原子类完成全部样式,无独立css文件
// Progress.ts
const Progress = ({ text, percentage, total }: {
text: string;
percentage?: number;
total?: number;
}) => {
// 容错:不传百分比默认赋值0
percentage ??= 0;
return (
<div className="w-full bg-gray-100 text-left rounded-lg overflow-hidden mb-0.5">
{/* 动态宽度控制进度 */}
<div
style={{ width: `${percentage}%` }}
className={`bg-blue-400 whitespace-nowrap px-1 text-sm`}
>
{text} {percentage.toFixed(2)}%
{/* 无总大小则不展示容量文本 */}
{isNaN(total) ? "" : ` of ${formatBytes(total)}`}
</div>
</div>
);
};
export default Progress;
3.3 组件踩坑提醒
四、实战2:WebGPU本地大模型完整页面App.tsx
页面实现功能:
// App.tsx
import { useState, useEffect } from 'react';
import Progress from '../components/Progress';
function App() {
// 输入框文本状态
const [input, setInput] = useState('');
// 模型状态:ready就绪 / loading下载中 / error报错
const [status, setStatus] = useState("ready");
// 错误信息存储
const [error, setError] = useState<null | string>(null);
// 下载提示文案
const [loadingMessage, setLoadingMessage] = useState("开始加载");
// 多模型文件下载进度数组
const [progressItems, setProgressItems] = useState<Array<{
text: string;
percentage?: number;
total?: number;
}>>([]);
// WebGPU兼容性判断
const IS_WEBGPU_AVALABLE = !!navigator.gpu;
// 发送对话回调
const onEnter = () => {
console.log('发送提问:', input);
// 后续可接入Transformers.js推理逻辑
};
// 组件挂载生命周期
useEffect(() => {
console.log('页面组件挂载完成');
}, []);
return (
IS_WEBGPU_AVALABLE ? (
<div className="flex flex-col h-screen mx-auto items-center justify-end text-gray-800 bg-white">
<div className="h-full overflow-auto flex justify-center items-center flex-col relative">
<div className="flex flex-col items-center mb-1 max-w-[400px] text-center">
<h1 className="text-4xl font-bold mb-1">DeepSeek–R1 WebGPU</h1>
<h2 className="font-semibold">
浏览器本地运行推理模型,无需后端、保护隐私
</h2>
</div>
<div className="flex flex-col items-center px-4">
<p className="max-w-[510px] mb-4">
加载 <a
href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX"
target="_blank"
rel="noreferrer"
className="font-medium underline"
>DeepSeek–R1 1.5B蒸馏轻量化模型</a>,基于Transformers.js+ONNX Web运行,所有数据仅保存在本地,支持离线对话。
</p>
{/* 错误提示区域 */}
{error && (
<div className="text-red-500 text-center mb-2">
<p className="mb-1">模型加载失败:</p>
<p className="text-sm">{error}</p>
</div>
)}
{/* 加载按钮,加载/报错状态禁用 */}
<button
className="border px-4 py-2 rounded-lg bg-blue-400 text-white hover:bg-blue-500 disabled:cursor-not-allowed select-none"
disabled={status !== "ready" || error !== null}
onClick={() => setStatus("loading")}
>
加载本地模型
</button>
</div>
</div>
{/* 模型下载进度列表 */}
{status === "loading" && (
<div className="w-full max-w-[500px] text-left mx-auto p-4 mt-auto">
<p className="text-center mb-1">{loadingMessage}</p>
{progressItems.map((item, i) => (
<Progress
key={i}
text={item.text}
percentage={item.percentage}
total={item.total}
/>
))}
</div>
)}
{/* 聊天输入框 */}
<div className="mt-2 border border-gray-300 rounded-lg w-[600px] max-w-[80%] max-h-[200px] mx-auto relative mb-3 flex">
<textarea
className="w-full px-3 py-4 rounded-lg bg-transparent border-none outline-hidden disabled:text-gray-400"
placeholder="输入你的问题…"
rows={1}
disabled={status !== "ready"}
value={input}
// 双向绑定模拟:onInput同步文本
onInput={(e) => {
const target = e.target as HTMLTextAreaElement;
setInput(target.value);
}}
// 回车发送,Shift+Enter换行
onKeyDown={(e) => {
if (input.length > 0 && e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onEnter();
}
}}
title={status === 'ready' ? '模型就绪,可以提问' : "请等待模型加载完成"}
/>
</div>
</div>
) : (
<div className="h-screen flex items-center justify-center text-xl text-gray-600">
当前浏览器不支持WebGPU,无法运行本地AI模型,请升级Chrome/Edge浏览器
</div>
)
);
}
export default App;
页面开发踩坑清单
五、端侧AI项目落地核心痛点解决方案
痛点1:用户浏览器不支持WebGPU
解决方案:
痛点2:大模型文件下载缓慢、重复下载
解决方案:
痛点3:模型下载过程页面卡死
解决方案: 使用Web Worker单独运行模型加载、推理逻辑,脱离主线程,UI操作不会阻塞
痛点4:进度数据状态混乱,多文件进度互相覆盖
解决方案: 使用数组存储每个文件独立进度对象,循环渲染独立进度条,每项数据隔离互不干扰
六、整体项目架构总结
1. 分层结构
- 页面层:App.tsx 负责整体布局、状态管理、交互逻辑
- 通用组件层:Progress.ts 可复用业务组件,纯展示接收Props
- 工具函数层:formatBytes 通用计算工具,解耦业务逻辑



