GitHub Copilot 运行时从 TypeScript 迁移到 Rust 的工程实践

本文详述GitHub Copilot将核心运行时从TypeScript/Node.js迁移至Rust的完整工程路径,涵盖迁移动因(延迟敏感、内存效率、并发瓶颈)、关键技术选型(tokio异步运行时、axum HTTP框架、serde序列化、reqwest流式通信、napi-rs与独立服务混合调用方案),以及实操五步法:性能基线测量、接口契约定义、核心逻辑Rust实现、并发与背压控制、错误处理与可观测性建设。重点强调Rust在确定性延迟、零GC、多核并行和类型安全上的结构性优势。

1. 从 TypeScript 到 Rust:一次运行时迁移背后的工程逻辑

GitHub Copilot 把运行时从 TypeScript/Node.js 迁到 Rust,这条消息在开发者圈子里炸开的速度,比很多人预想的要快。我第一时间看到的时候,脑子里冒出来的第一个念头不是"Rust 又赢了",而是"他们终于扛不住了"。为什么这么说?因为任何一个把 TypeScript + Node.js 作为核心运行时的团队,走到一定规模之后,都会撞上同一堵墙—— 性能天花板、内存占用、冷启动延迟、并发模型 ,这四个问题像四颗钉子,钉在每个 Node.js 重度项目的棺材板上。

Copilot 这个产品对延迟的敏感程度,跟普通 Web 应用完全不是一个量级。你在编辑器里敲一个字符,它要在几十毫秒内完成上下文收集、模型推理请求编排、结果流式返回、代码片段格式化这一整套流程。任何一个环节多花 10ms,用户就能明显感觉到"卡"。而 Node.js 的单线程事件循环模型,在高并发 + 计算密集的混合场景下,表现是相当挣扎的。

这篇文章我想做的事情很明确: 把"运行时迁移到 Rust"这件事拆开揉碎 ,从为什么迁、迁什么、怎么迁、迁完有什么坑,一层一层讲清楚。不管你是正在考虑给现有 Node.js 项目做性能优化的工程师,还是单纯想理解 Rust 在真实工业场景里怎么落地的人,这篇内容都能给你一些可以直接参考的东西。我不会只讲"Rust 很快"这种废话,而是会把 具体的架构决策、参数取舍、迁移路径、踩坑记录 都摆出来。

1.1 为什么是"运行时"而不是"全部重写"

先澄清一个容易被误读的点:Copilot 迁移的是 运行时(runtime) ,不是把整个产品用 Rust 重写一遍。这两者的工程量差了好几个数量级。

所谓运行时,指的是支撑核心业务逻辑执行的那一层基础设施——包括请求调度、上下文管理、与模型服务的通信、缓存、序列化/反序列化、流式输出处理等。UI 层、配置层、部分业务编排逻辑,很可能还是保留在 TypeScript 里。这种" 核心热路径用 Rust,外围逻辑保留 TS "的混合架构,是目前业界最务实的做法。

为什么不全量重写?因为全量重写的风险极高,而且收益递减。TypeScript 在开发效率、类型安全、生态成熟度上的优势,在非热路径上依然明显。真正需要 Rust 的,是那些 每秒被调用成千上万次、对延迟极度敏感、对内存分配极度敏感 的代码路径。

提示:判断一个模块要不要用 Rust 重写,我的经验法则是看三个指标——调用频率是否超过每秒 1000 次、单次执行是否涉及大量内存分配、是否处于用户可感知的延迟链路上。三个都满足,才值得动。

1.2 Node.js 到底卡在哪里

很多人对 Node.js 的性能问题理解停留在"单线程"这个层面,其实不够准确。Node.js 的单线程指的是 JavaScript 执行的主线程是单线程 ,但 I/O 操作是通过 libuv 的线程池异步处理的。所以纯 I/O 密集的场景,Node.js 表现其实不差。

Copilot 遇到的问题,是 计算密集 + 高并发 + 低延迟 三者叠加。具体来说:

  • JSON 序列化/反序列化 :Copilot 每次请求都要处理大量的上下文数据,JSON 的解析和生成在 Node.js 里是纯 JS 执行,占主线程。
  • 字符串处理 :代码补全涉及大量的字符串拼接、截断、格式化,V8 的字符串操作在数据量大时 GC 压力陡增。
  • 并发请求编排 :同时向多个模型服务发请求、做结果聚合,Node.js 的 Promise 编排在高并发下调度开销不可忽视。
  • 冷启动 :Serverless 或容器化部署场景下,Node.js 的启动时间加上依赖加载,动辄几百毫秒。

这些问题单独看都不致命,但叠在一起,就成了用户体验的杀手。而 Rust 恰好在这几个维度上都有结构性优势: 零成本抽象、无 GC、真正的多线程并行、极低的运行时开销 。

1.3 Rust 带来的不只是"快"

如果只把 Rust 当成"更快的 Node.js",那就太小看这次迁移了。Rust 带来的核心价值,我认为有四个层面:

第一是确定性的延迟 。Rust 没有 GC,意味着不会出现"大部分请求很快,偶尔某个请求因为 GC 停顿卡 200ms"这种情况。对于 Copilot 这种交互式产品,延迟的 稳定性 比平均延迟更重要。用户不会记得你平均响应 50ms,但一定会记得你偶尔卡的那一下。

第二是内存效率 。Node.js 进程的内存占用通常从几百 MB 起步,Rust 服务可以做到几十 MB。在需要横向扩展几千个实例的场景下,这个差距直接转化为成本。

第三是并发模型的表达力 。Rust 的 async/await 配合 tokio 运行时,可以真正做到多核并行处理,而不是像 Node.js 那样靠 cluster 模块 fork 多个进程来绕开单线程限制。

第四是类型系统带来的重构信心 。Rust 的 trait 系统和所有权模型,让大规模重构时的编译器检查极其严格。这在迁移过程中反而是优势——编译器会帮你抓出大量潜在问题。

2. 迁移的核心技术选型与架构拆解

聊完"为什么",接下来是"怎么做"。这部分我会把迁移过程中涉及的关键技术选型一个个拆开讲,包括每个选择背后的权衡。

2.1 异步运行时:为什么是 tokio

Rust 的异步生态里,tokio 是事实上的标准。Copilot 这种场景选 tokio 几乎是必然的,原因有几个:

  • 成熟的生态 :几乎所有主流的 Rust 网络库、数据库驱动、HTTP 客户端都优先支持 tokio。
  • 多线程调度器 :tokio 默认使用 work-stealing 的多线程调度器,能充分利用多核 CPU。
  • 丰富的同步原语 :Mutex、RwLock、Semaphore、Channel 等一应俱全,且都是异步友好的。

如果你从 Node.js 过来,可以把 tokio 理解成"Node.js 的事件循环 + libuv 线程池 + cluster 模块"的合体,但性能和控制力强得多。Node.js 里你要靠 cluster fork 多进程才能用满多核,tokio 直接在一个进程内就搞定了。

#[tokio::main]
async fn main() {
    // 默认多线程运行时,自动使用所有可用 CPU 核心
    let handle = tokio::spawn(async {
        // 异步任务
    });
    handle.await.unwrap();
}

注意:tokio 的默认多线程运行时适合 I/O 密集 + 中等计算负载。如果你的任务里有大量 CPU 密集计算,要用 tokio::task::spawn_blocking 把它挪到阻塞线程池,否则会拖慢整个调度器。

2.2 HTTP 层:axum 还是 actix-web

Rust 的 Web 框架选择,主流是 axum 和 actix-web 两个。Copilot 这类场景,我倾向于推荐 axum,理由如下:

维度 axum actix-web
生态整合 与 tokio/tower/hyper 无缝 自成一派
学习曲线 平缓,基于 tower 中间件 稍陡,Actor 模型概念
性能 极高 极高
中间件生态 tower 生态丰富 自有中间件体系
类型安全 提取器模式,编译期检查 运行时检查较多

axum 最大的优势是它建立在 tower 和 hyper 之上,这两个是 Rust 异步 HTTP 的底层基石。用 axum 意味着你能直接复用整个 tower 中间件生态,比如超时、限流、追踪、压缩这些,都有现成的。

use axum::{routing::post, Router, Json};
use serde::Deserialize;

#[derive(Deserialize)]
struct CompletionRequest {
    prompt: String,
    max_tokens: u32,
}

async fn complete(Json(req): Json<CompletionRequest>) -> Json<String> {
    // 处理补全请求
    Json(format!("processed: {}", req.prompt))
}

let app = Router::new().route("/v1/complete", post(complete));

2.3 序列化:serde 的零成本抽象

从 Node.js 迁到 Rust,JSON 处理是绕不开的一环。Rust 的 serde 生态是这方面的标杆。serde 的核心优势在于 编译期生成序列化代码 ,没有运行时的反射开销。

对比一下:Node.js 里 JSON.parse 和 JSON.stringify 是 V8 内置的,速度其实不慢,但它是 动态类型 的,每次都要做类型推断和属性查找。serde 在编译期就知道所有字段的类型和位置,生成的代码接近手写的最优解。

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Context {
    file_path: String,
    language: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    cursor_position: Option<u32>,
}

skip_serializing_if 这类属性,能让你精细控制序列化行为,减少不必要的字段传输。在 Copilot 这种每次请求都要传大量上下文的场景下,这些细节累积起来就是可观的性能收益。

2.4 与模型服务的通信:reqwest + 流式处理

Copilot 的核心是跟模型服务通信。Rust 里用 reqwest 做 HTTP 客户端,配合 tokio 的流式处理,可以实现真正的 边接收边处理 。

Node.js 里做流式处理,你要处理 Stream、背压、错误传播,代码写起来挺绕。Rust 的 Stream trait 配合 futures 库,表达力强很多:

use futures::StreamExt;

let resp = reqwest::get("https://api.example.com/stream").await?;
let mut stream = resp.bytes_stream();

while let Some(chunk) = stream.next().await {
    let chunk = chunk?;
    // 处理每个数据块,可以立即推送给客户端
    process_chunk(chunk);
}

这种流式处理对 Copilot 至关重要——用户希望看到补全内容 逐字出现 ,而不是等全部生成完再一次性显示。Rust 的流式处理在延迟和内存占用上都优于 Node.js 的等价实现。

2.5 跨语言调用:napi-rs 还是纯 Rust 服务

迁移过程中一个关键的架构决策是: Rust 代码怎么跟现有的 TypeScript 代码交互 。有两条路:

路线 A:napi-rs 做原生模块 。把 Rust 编译成 Node.js 原生扩展,TypeScript 直接 require 调用。优点是集成简单,缺点是仍然受限于 Node.js 的运行时,无法完全摆脱单线程和 GC。

路线 B:Rust 独立服务 。Rust 编译成独立二进制,通过 HTTP/gRPC 跟 TypeScript 层通信。优点是彻底解耦,Rust 服务可以独立扩展;缺点是增加了网络调用开销和部署复杂度。

Copilot 这种规模的产品,大概率走的是 路线 B 为主、路线 A 为辅 的混合方案。核心热路径做成独立 Rust 服务,一些轻量的工具函数用 napi-rs 嵌入。

提示:napi-rs 适合"计算密集但调用不频繁"的场景,比如某个复杂的解析函数。如果调用频率极高,网络调用的开销反而比进程内调用更可控,因为可以批量处理。

3. 实操迁移路径与关键环节实现

理论讲完了,这部分进入实操。我会按照一个真实的迁移项目会经历的阶段,把每一步的关键操作和注意事项讲清楚。

3.1 第一步:性能基线测量与热点定位

任何迁移都必须从 测量 开始。没有基线数据,你根本不知道迁移有没有效果,也不知道该迁哪里。

在 Node.js 侧,我常用的工具组合是:

  • clinic.js :诊断 CPU 和内存问题,能生成火焰图。
  • 0x :轻量级火焰图生成器。
  • Node.js 内置的 --prof :V8 的采样分析器。

具体操作上,先跑一轮压测,记录下 P50、P95、P99 延迟和内存占用。然后生成火焰图,找出占用 CPU 最多的函数。

# 用 clinic 做火焰图分析
clinic flame -- node server.js

# 或者用 0x
0x node server.js

火焰图里那些 宽而平 的区块,就是热点。通常你会发现,JSON 处理、字符串操作、正则匹配这几类会占据大量时间。这些就是优先迁移的目标。

注意:不要凭直觉判断热点。我见过太多团队拍脑袋说"肯定是数据库慢",结果火焰图一出来,80% 的时间花在 JSON 序列化上。数据说话。

3.2 第二步:接口契约定义与数据模型设计

确定要迁移的模块后,下一步是 定义清晰的接口契约 。这一步做不好,后面会反复返工。

我的做法是:先用 TypeScript 把要迁移的模块的输入输出类型完整写出来,然后用 serde 在 Rust 侧一一对应。关键是 数据模型要一次设计到位 ,因为 Rust 的类型系统比 TypeScript 严格得多,后期改数据结构成本很高。

// Rust 侧的数据模型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionContext {
    pub file_path: String,
    pub language: Language,
    pub prefix: String,
    pub suffix: String,
    pub cursor_line: u32,
    pub cursor_col: u32,
    pub related_files: Vec<RelatedFile>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Language {
    Rust,
    TypeScript,
    Python,
    Other(String),
}

注意 #[serde(rename_all = "lowercase")] 这种属性,它保证 Rust 的枚举序列化后跟 TypeScript 侧的字符串字面量一致。这类细节在跨语言通信里特别重要,一个大小写不匹配就能让你 debug 半天。

3.3 第三步:核心逻辑的 Rust 实现

数据模型定好后,开始写核心逻辑。这里我以"上下文收集与裁剪"这个典型环节为例,讲讲实现要点。

Copilot 每次请求都要从当前文件、相关文件、最近编辑历史里收集上下文,然后裁剪到模型能接受的 token 上限。这个逻辑在 Node.js 里通常是同步的字符串操作,在 Rust 里可以用更高效的方式实现。

pub fn collect_context(
    current_file: &str,
    cursor_pos: usize,
    max_tokens: usize,
) -> String {
    // 以光标为中心,向两侧扩展
    let prefix = &current_file[..cursor_pos];
    let suffix = &current_file[cursor_pos..];
    
    // 按行切分,优先保留靠近光标的内容
    let prefix_lines: Vec<&str> = prefix.lines().rev().take(50).collect();
    let suffix_lines: Vec<&str> = suffix.lines().take(50).collect();
    
    let mut result = String::with_capacity(max_tokens * 4);
    for line in prefix_lines.iter().rev() {
        result.push_str(line);
        result.push('\n');
    }
    for line in suffix_lines {
        result.push_str(line);
        result.push('\n');
    }
    result
}

这里有个关键优化点: String::with_capacity 预分配内存。Node.js 里字符串拼接是自动扩容的,Rust 里如果你不预分配, push_str 会触发多次重新分配。在热路径上,这个细节能省下可观的分配开销。

3.4 第四步:并发与背压处理

Copilot 的请求是高度并发的,而且每个请求内部可能还要并发调用多个下游服务。Rust 的并发模型在这里优势明显,但也需要正确使用。

一个常见的模式是用 tokio::select! 做超时控制,用 Semaphore 做并发限流:

use tokio::sync::Semaphore;
use std::sync::Arc;
use std::time::Duration;

let semaphore = Arc::new(Semaphore::new(100)); // 最多 100 个并发

async fn handle_request(
    sem: Arc<Semaphore>,
    ctx: CompletionContext,
) -> Result<String, Error> {
    let _permit = sem.acquire().await?;
    
    tokio::select! {
        result = call_model(&ctx) => result,
        _ = tokio::time::sleep(Duration::from_secs(5)) => {
            Err(Error::Timeout)
        }
    }
}

Semaphore 的 acquire 返回一个 permit,drop 时自动释放。这种 RAII 风格的资源管理,是 Rust 相比 Node.js 的一大优势——你不用担心忘记释放信号量。

注意: tokio::select! 会取消未完成的分支。如果 call_model 里有不可取消的操作(比如已经发出的网络请求),要小心处理。通常的做法是把不可取消的部分放到 spawn 出去的任务里。

3.5 第五步:错误处理与可观测性

从 Node.js 迁到 Rust,错误处理是最容易出问题的地方。Node.js 里你可以随便 throw ,Rust 里必须显式处理每一个 Result 。

我的建议是用 thiserror 定义领域错误,用 anyhow 处理应用层错误:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum CompletionError {
    #[error("context too large: {0} tokens")]
    ContextTooLarge(usize),
    
    #[error("model service unavailable")]
    ServiceUnavailable,
    
    #[error("invalid request: {0}")]
    InvalidRequest(String),
}

可观测性方面,Rust 的 tracing 生态非常成熟。配合 tracing-subscriber 和 OpenTelemetry,可以做到跟 Node.js 侧一样的分布式追踪。

use tracing::{info, instrument};

#[instrument(skip(ctx))]
async fn process_completion(ctx: CompletionContext) -> Result<String, CompletionError> {
    info!(file = %ctx.file_path, "processing completion");
    // ...
}

#[instrument] 宏会自动记录函数的入参和耗时,非常方便。这在排查性能问题时特别有用。

4. 常见问题与排查技巧实录

迁移过程中踩的坑,才是最有价值的部分。这部分我整理了几个典型问题和排查思路。

4.1 编译期问题速查

Rust 的编译器严格是出了名的,从 TypeScript 过来的人一开始会很不适应。下面是我整理的高频编译错误对照表:

错误类型 典型信息 解决思路
所有权转移 "value moved here" 用引用 & 或 clone()
生命周期 "does not live long enough" 明确标注生命周期或改用 owned 类型
借用冲突 "cannot borrow as mutable" 缩小借用作用域,或用 RefCell
trait 未实现 "trait bound not satisfied" 检查是否 derive 了需要的 trait
async 块 "future is not Send" 避免在 await 点持有非 Send 类型

最常见的坑是 在 async 函数里持有 MutexGuard 跨 await 点 。标准库的 Mutex 不是 Send 的,跨 await 会导致编译失败。解决方案是改用 tokio::sync::Mutex ,或者把锁的作用域缩小到不跨 await。

// 错误:标准库 Mutex 跨 await
let guard = std_mutex.lock().unwrap();
some_async_fn().await; // 编译失败

// 正确:用 tokio 的异步 Mutex
let guard = tokio_mutex.lock().await;
some_async_fn().await; // OK

4.2 性能问题排查

迁移到 Rust 后,性能不一定自动变好。我见过不少案例,Rust 版本反而比 Node.js 慢,原因通常是 用错了方式 。

常见的性能陷阱:

  • 过度 clone :为了绕开所有权检查而到处 .clone() ,导致大量内存分配。
  • 同步阻塞 :在 async 上下文里调用阻塞函数,拖垮整个调度器。
  • 锁竞争 :用一把大锁保护所有共享状态,并发度上不去。
  • 未预分配 :频繁的 Vec::push 和 String::push_str 触发重新分配。

排查工具推荐 cargo flamegraph ,能直接生成火焰图:

cargo install flamegraph
cargo flamegraph --bin my-service

对比 Node.js 和 Rust 的火焰图,你会发现 Rust 的火焰图通常更"扁平"——没有 V8 的 JIT 编译、GC 这些开销,热点更集中在业务逻辑本身。

4.3 跨语言通信的坑

如果采用 napi-rs 方案,跨语言通信有几个高频问题:

字符串编码 :Node.js 的字符串是 UTF-16,Rust 是 UTF-8。napi-rs 会自动转换,但转换有开销。如果数据量大,考虑用 Buffer 传二进制。

错误传播 :Rust 的 Result 要正确映射到 JavaScript 的异常。napi-rs 提供了 napi::Error 类型,但要注意错误信息的完整性。

线程安全 :napi-rs 的 #[napi] 函数默认在主线程执行。如果要做 CPU 密集计算,要用 #[napi(ts_return_type = "...")] 配合 AsyncTask 挪到线程池。

#[napi]
pub fn heavy_compute(input: String) -> AsyncTask<ComputeTask> {
    AsyncTask::new(ComputeTask { input })
}

4.4 部署与运维的注意事项

Rust 服务的部署跟 Node.js 有几个不同点:

二进制体积 :Rust 编译出的二进制默认比较大,用 --release 加上 strip 符号能显著减小。Cargo.toml 里配置:

[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = true
panic = "abort"

lto = true 开启链接时优化, codegen-units = 1 让编译器做更激进的优化, panic = "abort" 去掉 panic 展开的代码。这几个配置组合下来,二进制能小 30% 以上。

内存监控 :Rust 没有 GC,但内存泄漏依然可能发生(比如 Arc 循环引用)。用 jemalloc 或 mimalloc 作为分配器,配合 heaptrack 做内存分析。

优雅关闭 :Rust 服务要正确处理 SIGTERM,等待正在处理的请求完成。tokio 提供了 tokio::signal 模块:

use tokio::signal;

async fn shutdown_signal() {
    signal::ctrl_c().await.expect("failed to listen for ctrl-c");
    // 触发优雅关闭逻辑
}

4.5 团队协作与知识转移

技术迁移最大的挑战往往不是技术本身,而是 团队适应 。从 TypeScript 到 Rust,学习曲线陡峭,团队需要时间。

我的建议是:

  • 先做小范围试点 :选一个独立的、边界清晰的模块先迁,积累经验。
  • 建立代码规范 :Rust 的 clippy 是很好的起点,配置严格的 lint 规则。
  • 结对编程 :让熟悉 Rust 的人跟不熟悉的人结对,加速知识转移。
  • 文档沉淀 :把踩过的坑、常用的模式整理成内部文档。
# 开启严格的 clippy 检查
cargo clippy -- -D warnings -W clippy::pedantic

clippy::pedantic 会开启很多严格的检查,一开始可能报一堆警告,但长期看能显著提升代码质量。

5. 迁移后的收益评估与后续演进

迁移完成后,怎么评估效果?不能只看"感觉快了",要有数据支撑。

5.1 关键指标对比

我建议从这几个维度做前后对比:

指标 Node.js 基线 Rust 迁移后 提升幅度
P50 延迟 45ms 18ms 60%
P99 延迟 320ms 65ms 80%
内存占用 480MB 45MB 90%
QPS(单实例) 1200 8500 7x
冷启动时间 800ms 15ms 98%

这些数字是典型量级,具体项目会有差异。但趋势是明确的: Rust 在延迟稳定性、内存效率、吞吐量上都有数量级的优势 。

P99 延迟的改善尤其值得关注。Node.js 的 P99 通常被 GC 停顿拉高,而 Rust 没有 GC,P99 和 P50 的差距小得多。这对用户体验的影响是巨大的。

5.2 成本结构的改变

迁移到 Rust 后,成本结构会发生明显变化:

  • 计算成本下降 :同样的 QPS,需要的实例数大幅减少。
  • 内存成本下降 :单实例内存占用降低一个数量级。
  • 人力成本上升 :Rust 开发者的招聘和培养成本更高。
  • 开发速度变化 :初期变慢,后期因为类型安全反而可能变快。

这个权衡需要根据团队实际情况判断。如果团队规模小、迭代速度快,全量迁 Rust 可能不划算。如果产品已经成熟、性能是核心竞争力,那迁移的收益就很明显。

5.3 后续演进方向

迁移到 Rust 不是终点,而是新的起点。后续可以往几个方向演进:

WASM 化 :把部分 Rust 逻辑编译成 WebAssembly,在浏览器端直接运行。Copilot 的一些本地预处理逻辑,完全可以在客户端用 WASM 完成,进一步降低延迟。

更激进的并发模型 :Rust 的 async 生态还在演进,比如 io_uring 的支持,能进一步降低 I/O 开销。

与边缘计算结合 :Rust 的小体积和低资源占用,非常适合部署到边缘节点。把部分推理或预处理逻辑下沉到离用户更近的地方。

统一技术栈 :如果迁移效果好,可以考虑把更多模块逐步迁到 Rust,最终形成"Rust 核心 + TypeScript 外围"的稳定架构。

我在实际做类似迁移的时候,最大的体会是: 不要追求一步到位 。先把最痛的那个点解决掉,拿到数据,说服团队,再逐步扩大范围。技术决策从来不是纯技术问题,它涉及成本、风险、团队能力、业务节奏的综合权衡。Rust 是好东西,但它不是银弹,用对地方才是关键。

最后分享一个我踩过的坑:迁移初期我太执着于"纯 Rust",把所有东西都往 Rust 里塞,结果发现一些简单的配置解析、日志格式化逻辑,用 Rust 写反而更啰嗦。后来调整策略,只迁真正需要性能的部分,外围逻辑保留 TypeScript,整体效率反而更高。 混合架构不是妥协,而是务实 。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值