2944 字
15 分钟
把 .zprofile 和 .zshrc 拆开之后,我具体做了哪些优化

上一篇更像是在记录我为什么要把 .zprofile.zshrc 拆开,以及这件事背后的职责边界。那一篇偏思路,这一篇则更偏实现。

因为真正把配置整理顺手,靠的不是“知道应该分开”这件事本身,而是你最后写进文件里的那些具体代码,到底有没有把重复初始化、路径污染、启动变慢和环境不一致这些问题真正处理掉。

所以这一篇就不再重复讲“应该怎么分”,而是直接围绕我最后采用的几段配置来展开,逐条解释它们为什么放在这里,以及它们到底优化了什么。

先看 .zprofile:这里解决的是全局环境的一次性初始化#

我把 .zprofile 当成登录阶段的环境声明文件。它的任务很简单:把那些应该让整个会话继承的变量一次性准备好,而不是每开一个终端都重新导出一遍。

我保留下来的核心结构大致是这样:

Terminal window
# ==================== 基础环境与 UI 变量 ====================
export BROWSER=firefox
export TERM=alacritty
export QT_QPA_PLATFORMTHEME="qt5ct"
export GTK_THEME=adw-gtk3-dark
# ==================== 网络代理配置 ====================
export no_proxy="localhost,127.0.0.1,10.96.0.0/12,192.168.59.0/24,192.168.49.0/24,192.168.39.0/24,192.168.0.0/16,10.0.0.0/8,*.local"
export all_proxy=http://127.0.0.1:7890
export http_proxy=http://100.64.0.6:7890
export https_proxy=http://100.64.0.6:7890
# ==================== 开发环境与路径 ====================
export NVM_DIR="$HOME/.nvm"
export PNPM_HOME="$HOME/.local/share/pnpm"
export PUB_HOSTED_URL="https://pub.flutter-io.cn"
export FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"

这一段看起来很平常,但它实际解决的是两个长期存在的小问题。

第一,避免每次打开终端都重复执行同样的 export。这类变量几乎没有必要在每个交互式 shell 里重新声明一次,它们更像是登录会话的背景设定。

第二,确保 GUI 应用和 IDE 也能继承到同一套环境。比如代理、PNPM、NVM、Podman 套接字这类变量,如果只放在 .zshrc 里,很多图形程序其实根本拿不到。

这也是为什么我后来越来越不喜欢把所有东西都扔进 .zshrc:它会让你误以为“终端里能用就等于系统里都能用”,但真实情况往往不是这样。

.zprofile 里最值得保留的一个细节,是 PATH 去重#

如果要从这份配置里挑一个最容易被低估、但长期收益很高的小优化,我会选路径去重。

我最后保留的是这种写法:

Terminal window
typeset -U path
path=(
"$HOME/.local/bin"
"$HOME/go/bin"
"$PNPM_HOME"
$path
)
export PATH

这里关键在 typeset -U path

在 Zsh 里,pathPATH 对应的数组形式。给它加上 -U 之后,这个数组会自动去重。也就是说,就算你后来又 source 了一次相关脚本,或者某个工具链也往里面插了重复路径,最终结果也不会不断叠加出一串越来越长的 PATH。

这类问题平时不一定会立刻出错,但会慢慢把环境搞脏。你开一个终端看不出来,开几十次、加几套工具链、再配上某些会重复追加 PATH 的安装脚本,问题就会越来越明显。

我很喜欢这个优化的原因在于,它不是靠“约定不要重复添加路径”来避免污染,而是直接用 Shell 本身的能力把这件事做成默认正确。

工程里能靠机制保证正确的地方,尽量不要靠记忆力维持秩序。

外部环境脚本继续保留,但必须做存在性判断#

我最后还保留了这两行:

Terminal window
[[ -f "$HOME/.local/bin/env" ]] && . "$HOME/.local/bin/env"
[[ -f "$HOME/.cargo/env" ]] && . "$HOME/.cargo/env"

这看上去只是一点点防御式写法,但其实很实用。

因为这类环境脚本往往来自外部工具安装过程,比如 Rust、某些包管理器或者本地自定义脚本。它们不一定永远存在,也不一定总在同一台机器上都齐全。

如果你直接无条件 source,一旦脚本缺失,登录阶段就会平白多一个错误。把存在性判断写上,配置的可移植性和容错就会好很多。

再看 .zshrc:这里解决的是“每次打开终端时,如何尽量又快又完整”#

.zprofile 不同,.zshrc 真正要处理的是交互体验本身。

我最后保留的主体大致可以概括成四块:

  1. Powerlevel10k 瞬时提示
  2. 补全系统初始化与缓存优化
  3. NVM 懒加载
  4. SSH Agent 单例管理

这几块加在一起,真正影响的是你每天反复打开终端时的体感。

P10k instant prompt 的作用,不是更花,而是更快地“先亮起来”#

我把这段放在 .zshrc 顶部:

Terminal window
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi

这一段的意义不是主题美化,而是缩短用户感知到的等待时间。

很多终端“慢”的感觉,并不是所有初始化都真的很重,而是屏幕在初始化完成前长时间没有反馈。instant prompt 先把提示符尽早画出来,让终端看起来几乎是秒开的,然后剩下的初始化在后面继续完成。

它不是凭空让所有逻辑都消失了,但它能显著改善交互上的第一感受。这种优化在高频工具上非常值。

补全系统初始化,真正值得做的是减少重复成本#

我保留的补全初始化结构大概是这样:

Terminal window
fpath=("$HOME/.oh-my-zsh/custom/completions" $fpath)
autoload -Uz compinit
compinit
export ZSH_COMPDUMP="${ZSH_COMPDUMP:-${ZDOTDIR:-$HOME}/.zcompdump}"
export ZSH_DISABLE_COMPFIX=true
zstyle ':omz:update' frequency 7

这里最关键的不是 compinit 本身,而是围绕它做的减负。

compinit 是 Zsh 补全体系的入口,没有它很多补全能力都起不来。但它也是启动路径里常见的耗时点之一,所以我更在意的是怎么把它的附带开销压低。

ZSH_DISABLE_COMPFIX=true 的作用是跳过一些安全检查,避免每次都在补全初始化时做额外扫描。zstyle ':omz:update' frequency 7 则把 Oh-My-Zsh 的更新检查频率降到每七天一次,而不是频繁在启动时做无谓工作。

这类优化的特点是:它们单独看都不惊人,但每开一次 shell 都能省一点,积起来就非常有意义。

zcompile 不是炫技,它就是在把“下次再用”的成本提前摊掉#

我最后还保留了一段对补全缓存进行编译的逻辑:

Terminal window
zcompdump="${ZDOTDIR:-$HOME}/.zcompdump-${ZSH_VERSION}"
if [[ -s "$zcompdump" && (! -s "${zcompdump}.zwc" || "$zcompdump" -nt "${zcompdump}.zwc") ]]; then
zcompile "$zcompdump"
fi

这段第一次看可能会觉得有点“配置党味道太重”,但它本质很简单:

如果已有补全缓存文件,并且它比编译产物更新,就把它编译成 .zwc,让后续读取更快。

说白了,这就是把补全系统里那份会反复读取的数据做一次预处理。逻辑上跟很多构建缓存、字节码缓存并没有什么区别。

我比较喜欢这段的地方在于,它不是无脑每次都 zcompile,而是只在需要的时候才做。这就避免了“为了优化启动,反而每次启动都先跑一段额外逻辑”的反效果。

NVM 懒加载,是这套配置里最能明显改善体感的一项#

如果你装了 NVM,又在 .zshrc 里直接 source 它的主脚本,那终端启动变慢几乎是迟早的事。

我最后采用的是这类懒加载结构:

Terminal window
if [ -s "$NVM_DIR/nvm.sh" ]; then
_lazy_load_nvm() {
unset -f nvm node npm npx
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"
}
nvm() { _lazy_load_nvm; nvm "$@"; }
node() { _lazy_load_nvm; node "$@"; }
npm() { _lazy_load_nvm; npm "$@"; }
npx() { _lazy_load_nvm; npx "$@"; }
fi

这里的思路非常直接:

  • 开终端时不加载 NVM
  • 第一次真正执行 nodenpmnpxnvm 时,再完成初始化
  • 初始化后卸掉占位函数,让后续命令直接走真实实现

这类优化之所以有效,是因为你并不是每次打开终端都要立刻跑 Node 相关命令。很多时候你只是要 cdlsgit statusvimkubectlssh,那一整套 Node 环境初始化在这些场景里就是纯粹的启动负担。

把它延后到真正需要时再做,几乎是最合理的折中。

SSH Agent 单例管理,解决的是“我不想每次都手动加 key”#

这一段也是我比较想长期保留的:

Terminal window
SSH_ENV="$HOME/.ssh/agent-env"
_start_agent() {
ssh-agent -s > "${SSH_ENV}" 2>/dev/null
chmod 600 "${SSH_ENV}"
. "${SSH_ENV}" > /dev/null
for key in ~/.ssh/id_ed25519_{github,aur}_YangYuS8; do
[ -f "$key" ] && ssh-add "$key" 2>/dev/null
done
}
if [ -f "${SSH_ENV}" ]; then
. "${SSH_ENV}" > /dev/null
ps -p ${SSH_AGENT_PID} > /dev/null 2>&1 || _start_agent
else
_start_agent
fi
unset _start_agent

这段的目标不是“把 SSH Agent 弄复杂”,而是避免两种很常见的烦人情况:

  • 每开一个终端就起一个新的 agent
  • 需要用 GitHub 或 AUR key 时才发现还没 ssh-add

它的工作方式很朴素:

  1. 把 agent 的环境变量保存到一个文件里
  2. 新终端优先复用已有 agent
  3. 发现旧 agent 已经不存在时再重启
  4. 启动后自动把常用 key 加进去

对我来说,这种配置最大的价值不是“更高级”,而是它让 SSH 变成了一个默认可用的基础设施,而不是一个时不时打断工作流的前置步骤。

unsetopt correct_all 这种小设置,属于纯粹的减噪#

还有一行很小,但我仍然选择保留:

Terminal window
unsetopt correct_all

这不是性能优化,而是交互减噪。

Zsh 的自动纠错有时候确实能帮忙,但在命令行已经足够熟悉、补全也已经完善的前提下,它带来的干扰往往比收益更大。尤其是当你经常输入一些缩写命令、容器名、路径片段或者自定义函数时,这种“你是不是想输入另一个东西”的提示会很烦。

所以我最后的选择不是继续忍着,而是直接关掉。

很多优化并不一定是“让系统更强”,也可能只是“让系统少打扰你一点”。

这一套配置真正改善的,不是某个 benchmark,而是每天的摩擦感#

如果只看这些片段,它们都不算什么惊天动地的大招。PATH 去重、NVM 懒加载、补全缓存编译、SSH Agent 单例,这些东西都很朴素,甚至很多老用户都会觉得“这不是常规操作吗”。

但它们放在一起的价值,就在于把终端环境从“能用”往“低摩擦”推进了一步。

终端启动更快一点,路径不再越积越脏一点,Node 相关初始化不再拖累所有 shell 一点,SSH 密钥不再时不时掉链子一点。单项都不夸张,但日常使用里每一项都会反复出现。

工具链体验很多时候就是这样被塑形的。真正影响你感受的,未必是某一次大改,而是这些不那么显眼的小优化能不能被长期保留下来。

这篇和上一篇的区别,就在于它不只讲原则,而是把代码掰开来看#

上一篇我更想强调的是边界、结构和 AI 协作里该保持的警惕。这一篇我更想留下的,则是这套配置在代码层面具体长什么样,以及这些细节为什么值得保留。

因为最后真正陪你每天工作的,不是抽象结论,而是这些会在每次登录、每次开终端、每次调用 node、每次 git push 时实际执行的代码。

把这些小地方理顺,终端不会突然变成另一个世界,但它会明显更安静、更快、更接近你真正想要的那种工具状态。

把 .zprofile 和 .zshrc 拆开之后,我具体做了哪些优化
https://blog.yangyus8.top/posts/20260309-02/
作者
杨与S8
发布于
2026-03-09
许可协议
CC BY-NC-SA 4.0