Redux Essentials 实战:选择器记忆化、数据规范化与 createListenerMiddleware 响应式逻辑
本篇围绕 Redux Essentials 官方教程 Part 6 展开:在 React + Redux 应用中,如何用 createSelector 记忆化选择器消除无意义的组件重渲染,如何用 Redux Toolkit 的 createEntityAdapter 把数组存储重构为 {ids, entities} 规范化结构,以及如何用 createListenerMiddleware 编写响应于 dispatch 的副作用逻辑。读完后你将掌握一套完整的生产级性能优化与数据管理方案,并能结合本仓库源码理解其中间件管线与订阅机制的底层依据。
前置背景:为什么会出现这些问题
Part 6 建立在 Part 5 异步数据获取的基础上(参见 part-5-async-logic.md)。教程围绕一个带 posts、users、auth、notifications 四个 slice 的示例应用推进,先补齐用户页面和通知功能,再逐一暴露并修复两类典型缺陷:
- 选择器内部
filter()每次返回新数组引用,导致组件在每次 action 后都重渲染; - 列表以数组方式存储,父组件一渲染子项全部跟着渲染,且按 ID 查找只能线性遍历数组。
此外还有一个贯穿性的认识:本仓库核心 dispatch 实现(见 src/createStore.ts)中,reducer 执行完 currentState = currentReducer(currentState, action) 后,是否真正产生状态变化完全由 reducer 的返回值决定——"本次 dispatch 不需要任何状态变更"是 reducer 合法的选择。这正是后文"dispatch 了 action 但状态可以不变"这一结论的底层依据。
补齐应用功能:用户页面与服务端登录
添加用户列表与用户详情页
<UsersList> 遵循标准模式:用 useSelector 读取数据并 map 渲染:
import { Link } from 'react-router-dom'
import { useAppSelector } from '@/app/hooks'
import { selectAllUsers } from './usersSlice'
export const UsersList = () => {
const users = useAppSelector(selectAllUsers)
const renderedUsers = users.map(user => (
<li key={user.id}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))
return (
<section>
<h2>Users</h2>
<ul>{renderedUsers}</ul>
</section>
)
}
<UserPage> 从路由参数取 userId,渲染该用户的全部帖子。注意这里埋下了本节的"事故现场"——selectPostsByUser 在 selector 内部调用了 filter():
export const selectPostsByUser = (state: RootState, userId: string) => {
const allPosts = selectAllPosts(state)
// ❌ 危险模式!后文会解释原因
return allPosts.filter(post => post.user === userId)
}
路由与导航栏随后加上 /users、/users/:userId 两个入口(App.tsx 中注册 <Route>,Navbar.tsx 中加 <Link to="/users">Users</Link>)。
登录逻辑改为异步 thunk
原本 <LoginPage> 和 authSlice 只是 dispatch 客户端 action 记录用户名,实际需要向服务器发送登录请求。改造方式与 posts、users 一致:用 createAppAsyncThunk 定义 login/logout,slice 中删除对应 reducer,改在 extraReducers 中处理 thunk 生命周期:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { client } from '@/api/client'
import type { RootState } from '@/app/store'
import { createAppAsyncThunk } from '@/app/withTypes'
interface AuthState {
username: string | null
}
export const login = createAppAsyncThunk(
'auth/login',
async (username: string) => {
await client.post('/fakeApi/login', { username })
return username
}
)
export const logout = createAppAsyncThunk('auth/logout', async () => {
await client.post('/fakeApi/logout', {})
})
const initialState: AuthState = { username: null }
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {},
extraReducers: builder => {
builder
.addCase(login.fulfilled, (state, action) => {
state.username = action.payload
})
.addCase(logout.fulfilled, state => {
state.username = null
})
}
})
export default authSlice.reducer
<Navbar> 和 <LoginPage> 改为 dispatch 新 thunk;postsSlice 原本监听 userLoggedOut action creator,也相应改为监听 logout.fulfilled,在登出时清空帖子列表(return initialState)。
通知功能:从拉取数据到追踪已读状态
通知 Slice 与 thunk 参数
新建 notificationsSlice,异步 thunk fetchNotifications 从假 API 拉取"比最新一条更新"的通知——为此它需要读取当前 state 里最新通知的时间戳。这引出了 createAsyncThunk 的第二个参数 thunkAPI:
export const fetchNotifications = createAppAsyncThunk(
'notifications/fetchNotifications',
async (_unused, thunkApi) => {
const allNotifications = selectAllNotifications(thunkApi.getState())
const [latestNotification] = allNotifications
const latestTimestamp = latestNotification ? latestNotification.date : ''
const response = await client.get<ServerNotification[]>(
`/fakeApi/notifications?since=${latestTimestamp}`
)
return response.data
}
)
thunkAPI 对象包含五个关键成员:
| 成员 | 用途 |
|---|---|
dispatch / getState |
store 真实的方法,可在 thunk 中再 dispatch 或读取最新 state |
extra |
创建 store 时传给 thunk 中间件的"extra argument",通常是 API 封装函数 |
requestId |
本次 thunk 调用的唯一随机 ID,用于追踪单个请求的状态 |
signal |
AbortController.signal,可取消进行中的请求 |
rejectWithValue |
自定义 rejected action 内容的工具函数 |
注意细节:createAsyncThunk 的 payload creator 第一个参数是 dispatch 时传入的参数,只支持一个。若不传,该参数就是 undefined——所以必须给第一个形参占位(此处命名 _unused)才能访问到第二个参数 thunkApi。如果手写 thunk(不用 createAsyncThunk),则 dispatch 和 getState 是分开的独立参数传入。
slice 侧在 fetchNotifications.fulfilled 时把新通知 push 进 state 并按日期倒序排序。教程特别提醒:array.sort() 会原地修改数组,这里之所以安全,是因为 createSlice 内部使用 Immer 对 draft 状态进行写操作。
客户端元数据:read / isNew
"新消息/未读"只是客户端概念,因此定义扩展类型并在接收服务器数据时补默认值:
export interface ServerNotification {
id: string
date: string
message: string
user: string
}
export interface ClientNotification extends ServerNotification {
read: boolean
isNew: boolean
}
allNotificationsReadreducer:把所有通知标记为已读;fetchNotifications.fulfilled:新通知统一带read: false, isNew: true元数据;已存在的通知按"已读则不再是新"的语义更新isNew;- 新增选择器
selectUnreadNotificationsCount统计未读数量,供导航栏角标使用。
用 useLayoutEffect 标记已读:一个"两次 dispatch"的教学案例
<NotificationsList> 每次渲染都在 useLayoutEffect 中 dispatch allNotificationsRead()(用 useLayoutEffect 是为了避免数据闪烁):
useLayoutEffect(() => {
dispatch(allNotificationsRead())
})
这里出现了一个看似奇怪的现象:一次操作会 dispatch 两次 allNotificationsRead。原因链是:组件挂载 → 第一次 dispatch → reducer 不可变地更新条目,state.notifications 产生新数组引用 → useSelector 检测到新引用 → 组件重渲染 → useLayoutEffect 再跑一次、再次 dispatch → 这次 reducer 没有任何数据变化,slice 状态与根状态保持引用不变,组件不再重渲染。
这正是 Redux 的一个核心语义:dispatch 一个 action 完全可以不引起任何状态变化,是否更新状态是 reducer 的自主决定。结合本仓库 src/createStore.ts 的实现可以看到:dispatch 只是把 currentReducer 的返回值赋给 currentState 并通知订阅者,"是否变了"取决于 reducer 是否返回了新引用。教程结论是:可以拆分"挂载时 dispatch 一次 + 数组长度变化时再 dispatch"来避免第二次调用,但"反正无副作用就不必处理"也是合理取舍。
导航栏则用 selectUnreadNotificationsCount 渲染未读角标 <span className="badge">{numUnreadNotifications}</span>。
改进渲染性能:记忆化选择器
用 React Profiler 定位问题
在 <UserPage> 打开时点击 "Refresh Notifications",React DevTools Profiler 会显示:<Navbar> 重渲染合理(要更新未读角标),但 <UserPage> 也重渲染了——它并不读任何通知状态:
排查 selectPostsByUser 就能找到根因:filter() 每次调用都产生一个新数组引用。而 useSelector 的语义是:每次 action 后重跑 selector,若返回值与上次引用不等就强制组件重渲染。于是这个选择器导致组件在每个 action 之后都重渲染,即使帖子数据毫无变化。
React-Redux 在开发模式会对此发出警告:
Selector unknown returned a different result when called with the same parameters.
This can lead to unnecessary rerenders.
Selectors that return a new reference (such as an object or an array) should be memoized:
at UserPage (http://localhost:5173/src/features/users/UserPage.tsx)
这个示例 app 里影响不大,但在真实大型应用中,"不该渲染的组件额外重渲染"是常见且严重的性能问题。
用 createSelector 记忆化
"记忆化(memoization)"的思路是:缓存上一组输入和计算结果,输入没变就直接返回旧结果。Reselect 库为此设计,Redux Toolkit 直接 re-export 了其 createSelector。重写后的 selectPostsByUser:
import { createSlice, createAsyncThunk, createSelector } from '@reduxjs/toolkit'
export const selectAllPosts = (state: RootState) => state.posts.posts
export const selectPostsByUser = createSelector(
// 一个或多个"输入选择器"
[
selectAllPosts, // 读取根 state 中某个值的已有选择器
(state: RootState, userId: string) => userId // 提取调用参数
],
// 输出函数:输入值任一变化才重新执行
(posts, userId) => posts.filter(post => post.user === userId)
)
工作机制:调用 selectPostsByUser(state, userId) 时,createSelector 把全部参数传给每个输入选择器,输入选择器的返回值作为输出函数的实参。执行示例:
const state1 = getState()
// 第一次调用,输出函数执行
selectPostsByUser(state1, 'user1')
// 参数未变,输出函数不执行
selectPostsByUser(state1, 'user1')
// userId 变了,输出函数执行
selectPostsByUser(state1, 'user2')
dispatch(fetchUsers()) // 只改了 users,posts 没变
const state2 = getState()
// posts 和 userId 都没变,输出函数不执行
selectPostsByUser(state2, 'user2')
dispatch(addNewPost()) // posts 变了
const state3 = getState()
// posts 变了,输出函数执行
selectPostsByUser(state3, 'user2')
记忆化之后重跑 Profiler,<UserPage> 不再重渲染。
并非所有选择器都需要记忆化
判断标准:只有当选择器返回新建的对象/数组引用,或计算逻辑"昂贵"时才需要记忆化。以 selectUnreadNotificationsCount 为例——它内部虽有 .filter(),但返回的是数字(unreadNotifications.length),引用不会变化,作为普通函数就足够安全;当然把它也改成记忆化选择器可以省掉每次重新 filter 的开销,只是"必要性"远低于返回新引用的场景。
补充一个与本仓库文档一致的细节(见 docs/usage/deriving-data-selectors.md):createSelector 默认每个选择器实例只缓存最近一组参数。若同一个选择器要在多个位置以不同参数复用(如不同 postId),每次输入变化都会重跑输出函数——这时应考虑"选择器工厂"模式,或改用 createSelectorFactory。
优化列表渲染:三种选项与规范化
列表全量重渲染的成因
在 <PostsList> 中点击某条帖子的反应按钮时,Profiler 显示所有 <PostExcerpt> 都重渲染了:
原因是 React 的默认行为:父组件渲染时,会递归渲染其所有子组件。不可变更新一条帖子意味着 posts 数组引用变了,<PostsList> 必须重渲染,随后 React 继续向下渲染了每一个 <PostExcerpt>。
可选优化路径有三条:
React.memo:把<PostExcerpt>包进React.memo(),仅在 props 真正变化时重渲染——最简单有效;- 改为选择 ID 列表:
<PostsList>只选 ID 数组,子组件收postId自己useSelector取数据;配合useSelector(selectPostIds, shallowEqual)按内容比较,可跳过 ID 集合未变的渲染。前提是 slice 侧保持数组有序; - reducer 维护独立 ID 数组 + 实体查找表:ID 数组只在增删时变化,
<PostsList>仅因此重渲染。
第三条正是 createEntityAdapter 要解决的问题。
规范化状态结构
"规范化(normalization)"指:
- 每份数据在 state 中只有一份拷贝,无重复;
- 数据存入以 ID 为键的查找表(普通 JS 对象即可充当 map/dictionary);
- 通常另有一个该类型全部 ID 的数组。
{
users: {
ids: ["user1", "user2", "user3"],
entities: {
"user1": {id: "user1", firstName, lastName},
"user2": {id: "user2", firstName, lastName},
"user3": {id: "user3", firstName, lastName},
}
}
}
按 ID 取项从"遍历整数组 find()"变成 O(1) 的直接索引:
const userObject = state.users.entities[userId]
更多背景参见 Normalizing State Shape。
createEntityAdapter 的核心能力
createEntityAdapter 把一组条目装进 { ids: [], entities: {} } 结构,并生成配套工具。收益:
- 免去手写规范化维护代码;
- 内置 reducer 函数覆盖"批量添加/更新一项/删除多项"等常见用例;
- 可选
sortComparer(与Array.sort()相同签名)让 ID 数组保持有序,且仅在增删或排序变化时更新数组。
返回的 adapter 对象含三块能力:
| 成员 | 说明 |
|---|---|
| CRUD reducer 函数 | setAll、addOne、addMany、upsertOne、upsertMany、updateOne、removeOne、removeMany 等;既可作 action 的 case reducer,也可作为"mutating"辅助函数在其他 reducer 内调用 |
getSelectors(stateSliceSelector) |
传入"从根 state 取本 slice"的选择器,生成 selectAll、selectById、selectIds 等 |
getInitialState(extraFields?) |
生成空 {ids: [], entities: {}},可传入额外字段(如 loading 状态)合并进来 |
将 postsSlice 规范化
import {
createEntityAdapter,
EntityState
} from '@reduxjs/toolkit'
interface PostsState extends EntityState<Post, string> {
status: 'idle' | 'pending' | 'succeeded' | 'rejected'
error: string | null
}
const postsAdapter = createEntityAdapter<Post>({
// 按日期倒序
sortComparer: (a, b) => b.date.localeCompare(a.date)
})
const initialState: PostsState = postsAdapter.getInitialState({
status: 'idle',
error: null
})
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postUpdated(state, action: PayloadAction<PostUpdate>) {
const { id, title, content } = action.payload
const existingPost = state.entities[id]
if (existingPost) {
existingPost.title = title
existingPost.content = content
}
},
reactionAdded(
state,
action: PayloadAction<{ postId: string; reaction: ReactionName }>
) {
const { postId, reaction } = action.payload
const existingPost = state.entities[postId]
if (existingPost) {
existingPost.reactions[reaction]++
}
}
},
extraReducers(builder) {
builder
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
postsAdapter.setAll(state, action.payload)
})
.addCase(addNewPost.fulfilled, postsAdapter.addOne)
}
})
export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions
export default postsSlice.reducer
// 用 getSelectors 生成定制选择器,重命名以匹配旧名
export const {
selectAll: selectAllPosts,
selectById: selectPostById,
selectIds: selectPostIds
} = postsAdapter.getSelectors((state: RootState) => state.posts)
export const selectPostsByUser = createSelector(
[selectAllPosts, (state: RootState, userId: string) => userId],
(posts, userId) => posts.filter(post => post.user === userId)
)
逐点拆解:
PostsState从posts: Post[]变为继承EntityState<Post, string>(即{ids: string[], entities: Record<string, Post>}),保留status/error;- 实体进入
state.entities查找表后,reactionAdded、postUpdated直接以state.entities[postId]定位,不再遍历数组; fetchPosts.fulfilled中postsAdapter.setAll(state, action.payload)是"在 createSlice reducer 内以 mutating 方式调用 adapter 方法"的典型用法;addNewPost.fulfilled则把postsAdapter.addOne直接作为 case reducer 传入;- 手写选择器由
getSelectors的生成版本替换——因为选择器以根 state 调用,必须传入(state) => state.posts告知 slice 位置;生成函数固定叫selectAll/selectById,用解构重命名对齐旧导出名; - 进阶:
postUpdated可进一步简化为postsAdapter.updateOne(state, { id, changes: { title, content } });而reactionAdded不能用updateOne,因为它不是整字段替换,而是对嵌套计数的自增,直接查表加"mutating"更新更合适。
优化后的列表组件
<PostsList> 只选有序 ID 数组,<PostExcerpt> 收 postId 自行取实体:
interface PostExcerptProps {
postId: string
}
function PostExcerpt({ postId }: PostExcerptProps) {
const post = useAppSelector(state => selectPostById(state, postId))
// omit rendering logic
}
export const PostsList = () => {
const dispatch = useAppDispatch()
const orderedPostIds = useAppSelector(selectPostIds)
// omit other selections and effects
if (postStatus === 'pending') {
content = <Spinner text="Loading..." />
} else if (postStatus === 'succeeded') {
content = orderedPostIds.map(postId => (
<PostExcerpt key={postId} postId={postId} />
))
} else if (postStatus === 'rejected') {
content = <div>{postsError}</div>
}
// omit other rendering
}
再点一次反应按钮并捕获 Profiler,这次只有被点击的那一个 <PostExcerpt> 重渲染:
users 与 notifications 的规范化
usersSlice 只需三处改动:建 adapter、initialState = usersAdapter.getInitialState()、fetchUsers.fulfilled 直接用 usersAdapter.setAll 作 case reducer,选择器改由 getSelectors((state) => state.users) 生成。一个小坑:生成的 selectUserById 不接受可能为 null 的 currentUsername,selectCurrentUser 需要显式判空提前返回。
notificationsSlice 同理使用 notificationsAdapter(带 sortComparer 保持最新在前)。由于通知不再存于数组,需要全量遍历标记 isNew 时改用 Object.values(state.entities).forEach(...);而拉取落库逻辑则从手写 push+sort 换成 notificationsAdapter.upsertMany(state, notificationsWithMetadata)。
编写响应式逻辑:createListenerMiddleware
为什么需要监听中间件
此前所有行为都是"命令式"的:用户操作 → 点击处理器或 useEffect 里 dispatch。但有时逻辑应该是对已发生的事做出反应(如 addNewPost.fulfilled 后弹 toast 提示)。多个 slice 监听同一个 action 只能用于"再更新一部分 state";异步或含副作用的逻辑不能放进 reducer——reducer 必须纯粹、无副作用。而中间件正是 Redux 中为副作用而设计的扩展点,thunk 中间件解决"现在就执行"的异步逻辑,监听中间件解决"当特定 action 被 dispatch 时再执行"的响应逻辑。
createListenerMiddleware 可类比 React 的 useEffect:区别在于它定义在 Redux 逻辑层而非组件里,且响应的是 dispatch 的 action 与状态更新,而不是渲染生命周期。
配置监听中间件
新建 app/listenerMiddleware.ts,按 createAsyncThunk 的做法用 .withTypes() 固化 store 类型:
import { createListenerMiddleware, addListener } from '@reduxjs/toolkit'
import type { RootState, AppDispatch } from './store'
export const listenerMiddleware = createListenerMiddleware()
export const startAppListening = listenerMiddleware.startListening.withTypes<
RootState,
AppDispatch
>()
export type AppStartListening = typeof startAppListening
export const addAppListener = addListener.withTypes<RootState, AppDispatch>()
export type AppAddListener = typeof addAppListener
createListenerMiddleware() 返回对象的三个字段:
listenerMiddleware.middleware:真正的 Redux 中间件实例,需加入 store;listenerMiddleware.startListening:直接向中间件添加 listener 条目;listenerMiddleware.addListener:一个 action creator,可在任何能拿到dispatch的地方 dispatch 以动态添加 listener,无需导入 middleware 对象本身。
注册到 store:
import { configureStore } from '@reduxjs/toolkit'
import { listenerMiddleware } from './listenerMiddleware'
export const store = configureStore({
reducer: {
auth: authReducer,
posts: postsReducer,
users: usersReducer,
notifications: notificationsReducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware)
})
这里"顺序"很关键,可以从本仓库源码得到印证。中间件管线在 src/applyMiddleware.ts 中构建:
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
而 src/compose.ts 的 compose 是从右向左组合:compose(f, g, h) 等价于 (...args) => f(g(h(...args)))。因此 middlewares 数组中越靠前的中间件越外层、最先拦截 dispatch,链路形如 m1 -> m2 -> m3 -> store.dispatch()。监听中间件需要最先拦截并处理部分 action,所以放在管线头部:getDefaultMiddleware() 返回的是数组,configureStore 在其上扩展了 .concat() 的对称方法 .prepend(),专门把新中间件插到数组开头。thunk 中间件由 configureStore 默认内置(开发模式还附带安全检查中间件),prepend 保留默认集的同时把 listener 排在最前。
监听新帖并弹出 toast
约定把 listener 定义在逻辑最相关的 slice 文件中,但在 slice 里不直接导入 startAppListening(保持导入链一致),而是导出一个接收它的函数:
import { AppStartListening } from '@/app/listenerMiddleware'
export const addPostsListeners = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: addNewPost.fulfilled,
effect: async (action, listenerApi) => {
const { toast } = await import('react-tiny-toast')
const toastId = toast.show('New post added!', {
variant: 'success',
position: 'bottom-right',
pause: true
})
await listenerApi.delay(5000)
toast.remove(toastId)
}
})
}
然后在 app/listenerMiddleware.ts 末尾调用 addPostsListeners(startAppListening)——与 app/store.ts 从各 slice 导入 reducer 的方式同构。
添加 listener 时,匹配条件三选一:
| 选项 | 说明 |
|---|---|
actionCreator |
某个 RTK action creator,如 addNewPost.fulfilled;该 action 被 dispatch 时运行 effect |
matcher |
RTK matcher 函数,如 isAnyOf(reactionAdded, addNewPost.fulfilled);返回 true 时运行 |
predicate |
(action, currState, prevState) => boolean,可做任意检查,例如比较 currState.counter.value !== prevState.counter.value 检测状态变化 |
effect 回调形如异步 thunk:第一个参数是匹配到的 action,第二个是 listenerApi 对象——除了常规的 dispatch、getState,还提供 condition()(暂停直到其他 action 被 dispatch 或某状态值变化)、unsubscribe()/subscribe()(控制该 listener 条目的激活状态)、fork()(派生子任务)等,足以支撑复杂的异步工作流。
<App> 中还需渲染 react-tiny-toast 的 <ToastContainer />。最终效果:新增帖子成功后,右下角弹出绿色 toast,5 秒后消失——整个过程中 React 组件侧没有新增任何业务逻辑,响应完全由 store 中的监听中间件在 action 被 dispatch 后触发。
本篇要点回顾
- 记忆化选择器优化性能:Redux Toolkit re-export 的
createSelector生成记忆化选择器;仅当输入选择器返回新值时重算输出,既跳过昂贵计算又保证同一结果引用。 - 多种渲染优化模式可组合:避免在
useSelector中返回新对象/数组引用;传记忆化选择器给useSelector;用shallowEqual等替代比较函数;React.memo包裹组件;列表父组件只读 ID 数组、子组件按 ID 取实体。 - 规范化状态是推荐的存储方式:数据不重复、以
{ids: [], entities: {}}查找表组织。 createEntityAdapter承接规范化:sortComparer保持 ID 有序;getInitialState可合并 loading 状态;setAll/addMany/upsertOne/removeMany等内置 reducer;getSelectors生成selectAll/selectById。createListenerMiddleware运行响应式逻辑:需在 store 中以带类型的形式装配(用prepend置于管线头部);listener 通常定义在 slice 文件中;可按单 action、多 action 或自定义谓词匹配;effect 内可写任意同步/异步逻辑,listenerApi提供condition、fork、unsubscribe等编排工具。
后续章节将进入 RTK Query(见 part-7-rtk-query-basics.md),它是 Redux Toolkit 提供的数据获取与缓存 API,可完全省掉手写的获取逻辑;而 selector 的一般性写法与 createSelector 的进阶行为(缓存策略、嵌套记忆化等)可进一步参考 docs/usage/deriving-data-selectors.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


