FastAPI 与 Flask 深度对比:从 WSGI/ASGI 底层到选型决策
一句话结论:Flask 是「同步微框架 + 自己拼装」,FastAPI 是「异步框架 + 类型驱动 + 自带文档」。 差别不在功能表上谁多谁少,而在底层接口——Flask 建在 WSGI 上,FastAPI 建在 ASGI 上。
这篇文章不复述官方文档的功能清单,而是回答三个实际问题:
- 两者到底本质差在哪?
- FastAPI 是不是一定更快?
- 我手上这个项目该选哪个?
1. 本质分水岭:WSGI 与 ASGI
绝大多数「FastAPI vs Flask」的文章会从功能表讲起,但真正的分水岭是应用与服务器之间的接口协议。
1.1 Flask 与 WSGI:一次调用算完一个响应
WSGI(Web Server Gateway Interface,PEP 3333)规定:服务器把请求打包成 environ 字典交给应用,应用在一次调用里返回一个可迭代的字节序列。
注意 C → D → E → F 这一段:它是同步阻塞的。数据库要 20ms、外部 API 要 200ms,这段时间 CPU 什么都没干,但处理这个请求的线程必须原地等着。
想提高并发怎么办?只能加线程或加进程:
- 线程受 GIL 限制,且上下文切换有开销
- 进程吃内存,一个进程几百 MB
结论:I/O 越重的服务,WSGI 的性价比越低。
1.2 FastAPI 与 ASGI:等待时可以切走
ASGI(Asynchronous Server Gateway Interface)把接口改成基于协程的:应用是一个 async 可调用对象,接收 scope、receive、send 三个参数,可以反复 await。
关键在于:遇到 await 时,事件循环不会原地等,而是挂起当前请求、转去处理别的请求。等 I/O 完成后再回来继续。
这带来两个能力:
| 能力 | 说明 |
|---|---|
| 高并发 I/O | 单个 worker 就能承载成百上千个并发连接,不必一个请求一个线程 |
| 协议可扩展 | scope["type"] 可以是 http、websocket、lifespan,所以 WebSocket、长连接是天然支持的 |
1.3 等待的时间去哪了
判断该用哪种模型,关键不是「谁算得快」,而是等待期间谁被占着。下面这张图对比了同样 4 个并发请求、每个都要等 200ms I/O 的两种情况:
左边每等一次 I/O,就有一个线程被白白占住;右边只有一个事件循环,等待期间它转去处理别的请求。
把镜头拉近,看单个请求的切换时机:
同步模型下,A 和 B 只能一个等完再轮到另一个;异步模型下,等待时间被重叠。这不是「算得更快」,而是「等得不浪费」。
⚠️ 核心认知:
async不是加速器。它只对 I/O 密集 场景有效;对 CPU 密集计算(图像处理、加解密、复杂算法)毫无帮助,反而会因为单线程事件循环被拖垮。
2. 同一个接口,两种写法
抽象讲完,看具体代码。需求:POST /items 接收 {"name": "xxx", "price": 9.9},要求 name 非空、price 大于 0,成功返回 201。
2.1 Flask:校验、错误、状态码全手写
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post("/items")
def create_item():
data = request.get_json(silent=True) or {}
# 字段存在性 + 类型 + 业务约束,一层层自己判断
name = data.get("name")
if not isinstance(name, str) or not name:
return jsonify({"error": "name 必填且不能为空"}), 400
price = data.get("price")
if not isinstance(price, (int, float)) or isinstance(price, bool):
return jsonify({"error": "price 必须是数字"}), 400
if price <= 0:
return jsonify({"error": "price 必须大于 0"}), 400
# 真实项目里这里还要写:错误码规范、日志、文档……
return jsonify({"name": name, "price": price}), 201这段代码能跑,但它暴露了几个问题:
- 校验逻辑和业务逻辑混在一起,同一个模型在 10 个接口里要重复写 10 遍
- 错误响应格式随人而异,前端没法统一处理
- 接口文档是另一份文件,改代码忘了改文档是常态
2.2 FastAPI:把「长什么样」写成类型
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str = Field(min_length=1)
price: float = Field(gt=0)
@app.post("/items", status_code=201)
async def create_item(item: Item) -> Item:
return item对比一下,FastAPI 这段代码额外白送了:
| 能力 | Flask 版本 | FastAPI 版本 |
|---|---|---|
| 请求体解析 | 手写 request.get_json() | 自动 |
| 类型转换 | 手写 isinstance 判断 | 自动("9.9" → 9.9) |
| 字段约束 | 手写 if | Field(min_length=1, gt=0) |
| 错误响应 | 手写,格式随意 | 自动 422,带字段级 loc / msg / type |
| 响应过滤 | 手写字典 | response_model 自动裁字段 |
| 接口文档 | 需装扩展单独维护 | 自动生成 /docs、/redoc、/openapi.json |
| 编辑器补全 | 无 | item. 之后自动补全 name / price |
2.3 一个容易忽略的差别:函数签名即文档
Flask 的参数来自 request 对象,是运行时才能知道的事情;FastAPI 的参数来自函数签名,是静态可读的。
这意味着 FastAPI 的参数来源可以被框架推导出来:
Flask 要达到同样效果,得引入 flask-smorest、webargs 之类的库,而且依然绕不开手写解析。
3. 能力来源不同:内置 vs 拼装
这是我认为两者最本质的工程体验差异。Flask 是微框架,哲学是「给你最小内核,其余自己选」;FastAPI 是「内核该有的都给你,但 ORM 这类你仍然自己选」。
注意:FastAPI 不自带 ORM 和迁移,这一点常被误解。它和 Flask 一样要自己搭配 SQLAlchemy + Alembic,只是数据校验、文档、依赖注入、WebSocket 这几块省掉了。
3.1 依赖注入:FastAPI 最被低估的能力
数据库会话、当前登录用户、分页参数、权限校验——这些横切关注点在 Flask 里通常靠装饰器、g 对象或蓝图钩子来凑。FastAPI 有一套声明式的依赖注入:
from typing import Annotated
from fastapi import Depends, HTTPException
CurrentUser = Annotated[User, Depends(get_current_user)]
def require_role(role: str):
def checker(user: CurrentUser) -> User:
if user.role != role:
raise HTTPException(status_code=403, detail="权限不足")
return user
return checker
@app.delete("/posts/{post_id}")
async def delete_post(post_id: int, user: Annotated[User, Depends(require_role("admin"))]):
...Depends 还天然支持嵌套、请求内缓存和测试覆盖(app.dependency_overrides),这是 Flask 生态里要额外找库才能达到的效果。
4. 逐项对比总表
| 维度 | Flask | FastAPI |
|---|---|---|
| 底层接口 | WSGI(同步) | ASGI(异步,协程) |
| 首个版本 | 2010 年 | 2018 年 |
| 定位 | 微框架,内核极小 | 现代 API 框架 |
| 异步视图 | 2.x 起支持 async def,但底层仍是 WSGI,收益有限 | 一等公民,async def / def 都可,语义明确 |
| 数据校验 | 手工解析,或另配 Marshmallow / pydantic | 类型注解 + Pydantic 内置,自动 422 |
| 接口文档 | 需扩展(APIFlask、flask-smorest) | 内置 OpenAPI + Swagger UI / ReDoc |
| 依赖注入 | 无(靠 g、装饰器、扩展) | 内置 Depends,含子依赖与缓存 |
| WebSocket | 需 flask-sock 等扩展 | 原生支持 |
| ORM | 不自带 | 不自带(同样选 SQLAlchemy / SQLModel) |
| 模板渲染 | Jinja2 内置,成熟 | 支持 Jinja2,但非主打 |
| 请求校验失败响应 | 自己定 | 标准 422 + 字段级错误定位 |
| 生态成熟度 | 十几年积累,轮子多、Stack Overflow 答案多 | 生态快速补齐,但选型需自己拍板 |
| 学习曲线 | 极平缓,会 Python 就能写 | 需理解类型注解、Pydantic、依赖注入、async |
| 典型用户 | 小工具、内部系统、传统 Web、教学 | API 服务、AI/LLM 后端、微服务、数据服务 |
5. 性能真相:FastAPI 一定更快吗?
不一定。 这是最容易被营销话术误导的地方。分清三种情况:
5.1 情况一:I/O 密集型并发 → FastAPI 明显更好
大量请求在等数据库、等外部 API、等模型推理时,FastAPI 的事件循环能把等待时间重叠起来,同样的硬件能扛住高得多的并发。这是 FastAPI 的主场。
5.2 情况二:CPU 密集型 → 两者差不多
如果你的接口主要在算(图片处理、加解密、复杂算法),异步帮不上忙。而且在 FastAPI 里把 CPU 密集任务写成 async def 会更糟——它会占住唯一的事件循环,把所有其他请求一起卡死。
# ❌ 在 async 函数里做阻塞操作:整个事件循环被卡住
@app.get("/bad")
async def bad():
time.sleep(3) # 同步 sleep
requests.get("https://...") # 同步 HTTP 客户端
return {"ok": True}# ✅ 用异步库,或者干脆写成 def 交给线程池
@app.get("/good")
async def good():
await asyncio.sleep(3)
async with httpx.AsyncClient() as client:
await client.get("https://...")
return {"ok": True}关键规则:用异步库就写 async def,用同步库就写 def。FastAPI 会自动把 def 函数丢进线程池执行,不会阻塞事件循环——这一点 Flask 用户很容易忽略。
5.3 情况三:单个简单请求的延迟 → 差距很小
一个只返回 {"hello": "world"} 的接口,两者的延迟差别在真实业务里基本可以忽略。框架不是瓶颈,数据库和外部调用才是。
💡 一句话:FastAPI 的优势在并发吞吐,不在单请求速度。如果你的服务 QPS 只有个位数,性能不构成选型理由。
6. 选型决策
把前面的分析收敛成一张决策树:
6.1 明确选 Flask 的场景
- 几十行的小工具、内部脚本、Webhook 接收端——不必为类型注解和异步付出认知成本
- 团队已重度投入 Flask 专属扩展(Flask-Admin、Flask-Login、Flask-Security 那一套),迁移成本大于收益
- 以服务端模板渲染为主的传统内容站
- 教学场景:Flask 的心智负担确实更低
6.2 明确选 FastAPI 的场景
- 新写的 API 服务,需要给前端、移动端或第三方提供接口(OpenAPI 能直接生成客户端代码)
- AI / LLM 后端:大量调模型、调外部 API、查向量库,等待时间远大于计算时间
- 需要 WebSocket 或 SSE 流式输出(大模型流式响应几乎必然用到)
- 微服务 / 数据服务:依赖注入 + Pydantic 让分层与测试都更清爽
6.3 两个都别选,去选 Django
需要开箱即用的后台管理、ORM、迁移、认证、权限、审计一体化的内容型项目,Django admin 能省掉几周,FastAPI 得从零搭。别用「FastAPI 更现代」说服自己重造后台。
7. 常见误区澄清
| 误区 | 事实 |
|---|---|
| 「FastAPI 自带 ORM」 | 不自带。仍然要自己选 SQLAlchemy / SQLModel,迁移还是要 Alembic |
| 「用了 async 就自动变快」 | 只对 I/O 密集有效;CPU 密集无收益,写错反而更慢 |
| 「FastAPI 能直接替代 Django」 | 缺 admin、ORM、模板优先等一体化能力,重后台项目不合适 |
| 「Flask 支持 async 就和 FastAPI 一样了」 | Flask 的 async 视图底层仍是 WSGI,事件循环模型受限,收益有限 |
| 「FastAPI 性能一定碾压 Flask」 | 单请求延迟差距很小;优势体现在高并发 I/O 场景的吞吐 |
| 「Flask 过时了」 | 没有。它仍在积极维护,且是大量存量系统的合理选择 |
8. 从 Flask 迁移到 FastAPI 的注意点
如果你决定迁移,装饰器路由的写法相似,但下面几处需要重写思路:
| Flask 写法 | FastAPI 对应写法 |
|---|---|
@app.route("/x", methods=["POST"]) | @app.post("/x") |
request.args.get("q") | 函数参数 q: str | None = None |
request.get_json() | Pydantic 模型参数 |
jsonify(...) | 直接 return dict 或模型实例 |
abort(404) | raise HTTPException(404, ...) |
@login_required | Depends(get_current_user) |
app.before_request | 中间件或全局依赖 |
⚠️ 最容易翻车的一点:把同步数据库驱动(如
pymysql、psycopg2)直接搬进async def里。这会让整个事件循环阻塞,性能比 Flask 还差。要么换异步驱动(asyncpg、aiomysql),要么把这些函数写成def。
9. 小结
| 结论 | 说明 |
|---|---|
| 分水岭是接口协议 | WSGI 同步阻塞 vs ASGI 协程并发,这决定了后面的全部差异 |
| 差别在工程体验 | FastAPI 把校验、文档、依赖注入、WebSocket 做成了内置能力 |
| 性能要看场景 | I/O 密集高并发 FastAPI 优势明显;CPU 密集和低 QPS 场景差距不大 |
| 选型看项目性质 | API 优先、I/O 密集、需要实时通信 → FastAPI;极简小工具、存量生态 → Flask;重后台一体化 → Django |
| 没有银弹 | FastAPI 写得不对(async 里塞阻塞调用)会比 Flask 更慢 |
如果只记一句话:选 Flask 是因为「够用且简单」,选 FastAPI 是因为「接口优先且 I/O 密集」,选 Django 是因为「要的是完整的后台系统」。三者不是新旧替代关系,而是面向不同问题的工具。
📚 延伸阅读:FastAPI 的完整实战教程(19 章,从路由与参数到 SQLAlchemy 2.0、JWT 认证、测试与 Docker 部署)见 FastAPI 学习笔记。