学习
实践
活动
工具
TVP
写文章
专栏首页云原生实验室我就感觉到快 —— zsh 和 oh my zsh 冷启动速度优化

我就感觉到快 —— zsh 和 oh my zsh 冷启动速度优化

Docker 技术鼻祖系列

不论是在 WSL、Linux 还是 macOS 上,强大的 zsh 一直是我的不二法宝,而 oh my zsh 自然成了最趁手的瑞士军刀,我自己还编写了数个 oh my zsh 插件和主题。直到有一天我突然发现:见鬼,为什么开个 iTerm2 的 Tab 要等上好几秒钟?

1. zsh 启动耗时测量

首先,我们需要一个客观衡量 zsh 启动速度的标准,而使用 macOS 和众多 Linux 发行版中自带的 time 可以轻松计算任何命令的执行用时,包括 shell:

$ /usr/bin/time /bin/zsh -i -c exit

        1.77 real         1.04 user         0.95 sys

time 输出了 zsh 启动时 user-land 和 system 用时,而我的 zsh 启动用时将近 2 秒钟。为了获得更精确的结果,使用 for 循环连续启动 zsh 5 次:

$ for i in $(seq 1 5); do /usr/bin/time /bin/zsh -i -c exit; done

        1.74 real         1.02 user         0.92 sys
        1.69 real         1.00 user         0.90 sys
        1.71 real         1.01 user         0.91 sys
        1.68 real         0.99 user         0.89 sys
        1.74 real         1.02 user         0.93 sys

为了排除 zsh 本身的性能问题,使用 zsh 的 --no-rcs 参数进行测试:

$ for i in $(seq 1 20); do /usr/bin/time /bin/zsh --no-rcs -i -c exit; done

        0.00 real         0.00 user         0.00 sys
        0.00 real         0.00 user         0.00 sys
        0.00 real         0.00 user         0.00 sys
        0.00 real         0.00 user         0.00 sys
        0.00 real         0.00 user         0.00 sys

不加载 .zshrc 时,zsh 的启动速度是如此的快,以至于 time 给出了 0.00 的结果。

2. Profiling

zsh 提供了专门的 profiling 模块 zprof 用于衡量 zsh 各个函数的执行用时。在 .zshrc 文件第一行添加下述命令用于加载 zprof 模块:

zmodload zsh/zprof

接着启动 zsh、并使用 zprof 命令获取各函数用时数据:

$ /bin/zsh
$ zprof

num  calls                time                       self            name
-----------------------------------------------------------------------------------
 1)    1         395.66   395.66   33.10%    395.59   395.59   33.09%  _zsh_nvm_auto_use
 2)    1         216.22   216.22   18.09%    216.13   216.13   18.08%  nvm_die_on_prefix
 3)    1         648.00   648.00   54.20%    168.85   168.85   14.12%  nvm_auto
 4)    2         479.15   239.57   40.08%    160.50    80.25   13.43%  nvm
 5)    1         102.30   102.30    8.56%     84.99    84.99    7.11%  nvm_ensure_version_installed
 6)    2          51.21    25.60    4.28%     29.55    14.78    2.47%  compinit
 7)    1         680.18   680.18   56.89%     22.17    22.17    1.85%  _zsh_nvm_load
 8)    2          21.66    10.83    1.81%     21.66    10.83    1.81%  compaudit
 9)    1          17.31    17.31    1.45%     17.31    17.31    1.45%  nvm_is_version_installed
10)  193          17.43     0.09    1.46%     14.50     0.08    1.21%  _zsh_autosuggest_bind_widget
[Redacted]

zprof 模块只能获取每个 zsh 函数的用时,因此适合找出拖累 zsh 冷启动的 oh my zsh 的插件。如果要获取完整的 .zshrc 性能分析,应该使用 xtrace。在 .zshrc 开头添加如下命令:

zmodload zsh/datetime
setopt PROMPT_SUBST
PS4='+$EPOCHREALTIME %N:%i> '

logfile=$(mktemp zsh_profile.7Pw1Ny0G)
echo "Logging to $logfile"
exec 3>&2 2>$logfile

setopt XTRACE

并在 .zshrc 结尾添加如下命令:

unsetopt XTRACE
exec 2>&3 3>&-

这会在 $HOME 目录下生成一个文件名包含随机字符串的文件(zsh_profile.123456 )。一些介绍 zsh profiling 的文章会推荐使用 kcachegrind[1] 这个工具可视化这个文件,但是我们只需要知道是什么拖累了 zsh 冷启动,将这个文件格式化一下即可。这里提供一个简单的脚本:

#!/usr/bin/env zsh

typeset -a lines
typeset -i prev_time=0
typeset prev_command

while read line; do
    if [[ $line =~ '^.*\+([0-9]{10})\.([0-9]{6})[0-9]* (.+)' ]]; then
        integer this_time=$match[1]$match[2]

        if [[ $prev_time -gt 0 ]]; then
            time_difference=$(( $this_time - $prev_time ))
            lines+="$time_difference $prev_command"
        fi

        prev_time=$this_time

        local this_command=$match[3]
        if [[ ${#this_command} -le 80 ]]; then
            prev_command=$this_command
        else
            prev_command="${this_command:0:77}..."
        fi
    fi
done < ${1:-/dev/stdin}

print -l ${(@On)lines}

将上述内容保存在 $HOME 目录下 format_profile.zsh 文件中,然后在终端中执行:

$ cd $HOME
$ chmod +x format_profile.zsh
$ ./format_profile.zsh zsh_profile.123456 | head -n 30

356910 _zsh_nvm_auto_use:14> [[ none != N/A ]]
307791 /Users/sukka/.zshrc:312> hexo '--completion=zsh'
178444 /Users/sukka/.zshrc:310> thefuck --alias
161193 nvm_version:21> VERSION=N/A
148555 nvm_version:21> VERSION=N/A
96497 (eval):4> pyenv rehash
58759 /Users/sukka/.zshrc:311> pyenv init -
48629 nvm_auto:15> VERSION=''
42779 /Users/sukka/.zshrc:114> FPATH=/usr/local/share/zsh/site-functions:/usr/local...
42527 nvm_auto:15> nvm_resolve_local_alias default
41620 nvm_resolve_local_alias:7> VERSION=''
35577 nvm_resolve_local_alias:7> VERSION=''
29444 _zsh_nvm_load:6> source /Users/sukka/.nvm/nvm.sh
24967 compaudit:154> _i_wfiles=( )
24889 nvm_resolve_alias:15> ALIAS_TEMP=''
22000 nvm_auto:18> nvm_rc_version
20890 nvm_ls:29> PATTERN=default
[Redacted]

这样就一目了然了。可以看到,除了 nvm 以外、hexo 的自动补全、thefuck 的初始化、pyenv 都大幅拖慢了 zsh 的启动速度。

3. Lazyload

你可能听过 网页的图片可以 lazyload[2]Disqus 评论系统可以 lazyload[3],但是 .zshrc 一样也有 lazyload。lazyload 的特点是启动时快,首次使用时慢,因此很适合用于优化不常用而且初始化非常耗时的功能。

lazyload 的方法是声明一个占位函数,当执行这个函数时完成对真实命令的初始化、并移除命令占位。以 pyenv 为例:

export PATH="/Users/sukka/.pyenv/shims:${PATH}"

pyenv() {

  unfuntion pyenv


  eval "$(command pyenv init -)"


  pyenv "$@"
}

pyenv 在初始化时会自动加载补全(completion),但是由于 lazyload、第一次执行 pyenv 时就没有补全了,因此还需要为补全添加 lazyload:

__lazyload_completion_pyenv() {

  comdef -d pyenv

  unfunction pyenv

  source "$(brew --prefix pyenv)/completions/pyenv.zsh"
}

compdef __lazyload_completion_pyenv pyenv

这样,当首次输入 pyenv 并按下 Tab 时会加载 pyenv 的命令补全,第二次按下 Tab 时就可以正常显示命令补全了。

将上述 lazyload 封装成函数便于调用:

sukka_lazyload_add_command() {
    eval "$1() { \
        unfunction $1 \
        _sukka_lazyload__command_$1 \
        $1 \$@ \
    }"
}

sukka_lazyload_add_completion() {
    local comp_
    eval "${comp_name}() { \
        compdef -d $1; \
        _sukka_lazyload_completion_$1; \
    }"
    compdef $comp_name $1
}

使用封装好的 lazyload 函数添加 pyenvthefuck 的 lazyload、Hexo completion 的 lazyload:

_sukka_lazyload__command_pyenv() {

  eval "$(command pyenv init -)"
}
_sukka_lazyload__compfunc_pyenv() {

  source "$(brew --prefix pyenv)/completions/pyenv.zsh"
}

sukka_lazyload_add_command pyenv
sukka_lazyload_add_completion pyenv

_sukka_lazyload__command_fuck() {

  eval $(thefuck --alias)
}

sukka_lazyload_add_command fuck

_sukka_lazyload__completion_hexo() {

  eval $(hexo --completion=zsh)
}

sukka_lazyload_add_completion hexo

4. 替换 NVM

我使用 nvm 的方式是 zsh-nvm 插件。由于我的开发环境也高度依赖 .nvmrc 文件,所以不得不启用 nvm auto use。由于我的许多工具高度依赖 Node.js(如我的 Nali CLI[4]),lazyload nvm 也是不现实的。我不得不寻找另一个代替 nvm 的 Node.js 版本管理器,最后我选中了 `tj/n`[5]

首先是卸载 nvm、nvm 安装的所有 Node.js 版本、以及 zsh-nvm 插件:

$ rm -rf $HOME/.nvm


$ rm -rf $ZSH/custom/plugins/zsh-nvm

接着安装 tj/n 作为 Node.js 版本管理器,macOS 上可以通过 Homebrew 直接安装:

$ brew install n

.zshrc 中配置 tj/n

export N_PREFIX="$HOME/.n"

export N_PRESERVE_NPM=1

export PATH = "$N_PREFIX/bin:$PATH"

5. 使用 zsh 内置语法

zsh 强大之处不仅在于内建的插件、优雅的使用方式,更重要的是极其强大的语法。在 .zshrc 广泛使用 zsh 内置的语法可以大幅提高执行性能。

zsh 判断命令是否存在

我们经常需要在 .zshrc 之中编写命令是否存在的条件语句,比如「仅当命令存在时加载该命令的自动补全」,或者「当 Node.js 存在时输出 Node.js 版本」。通常情况下我们会写出以下三种条件判断方式:

[[ command -v node &>/dev/null ]] && node -v
[[ which -a node &>/dev/null ]] && node -v
[[ type node &>/dev/null ]] && node -v

但是在 zsh 中,还有一种速度更快的判断命令存在的方法:

(( $+commands[node] )) && node -v

zsh 提供了一个数组元素查找语法 +array[item] (元素存在则返回 1 否则返回 0),同时 zsh 也维护了一个命令数组 commands,在数组中检索元素比调用 which、type、command -v 命令都要快许多。

变量字符串查找

在 .zshrc 中鲜少需要用到这样的语法,不过依然存在一些 case,比如为了避免向 FPATH 中重复添加 Homebrew 的自动补全,提前检查 FPATH 中是否已经包含了 Homebrew 的路径。一般常见的写法都涉及到 echo 和 grep :

[[ $(echo $FPATH | grep "/usr/local/share/zsh/site-functions") ]] && echo "homebrew exists in fpath"

但是在 zsh 中我们不需要 grep 也可以实现同样的功能:

(( $FPATH[(I)/usr/local/share/zsh/site-functions] )) && echo "homebrew exists in fpath"

zsh 内置了在变量中匹配字符串的语法:variable[(i)keyword] 和 variable[(I)keyword],前者是从左往右寻找、后者是从右往左寻找,返回值为第一个匹配的首字符位置,当没有匹配时返回值则是变量的最终位置,也就是说当找不到匹配时 (i) 会返回字符串的长度、而 (I) 会返回 0。因此只需要从右往左寻找、判断返回值是否为 0 即可,搭配将数字转化为布尔值的 (( )) 就可以写出又快又漂亮的条件语句。

变量字符串替换

当需要截断或者替换字符串时,大部分人第一时间会想到 sed ,因当此需要替换变量中的字符时自然而然的会使用 echo | sed。比如,在 macOS 中主机名 $HOST 变量通常以 .local 结尾:

$ echo $HOST

Sukka-MBP.local

如果要显示 Sukka-MBP (在 prompt 中常常会用到)就需要写成:

$ echo $HOST | sed -e "s/.local//"

Sukka-MBP

但是,强大的 zsh 内置了简单的变量字符串替换语法,使用下述命令可以达到相同的效果:

$ echo ${HOST/.local/}

Sukka-MBP

$ echo ${HOST/.local/.foxtail}

Sukka-MBP.foxtail

6. 其它优化手段

禁用多余的插件

oh my zsh 在 Wiki 里说「Add wisely, as too many plugins slow down shell startup」。通过 profiling 可以发现一些插件(如 git 插件)执行耗时也不短。考虑到 oh my zsh 内置的 git 插件只是一些 alias、大部分我都用不到,因此将其从 plugins 数组中移除。

避免产生子进程

在 shell 中有不少语法会产生子进程。由于这些不受控制的子进程可能会产生其它子进程、从而导致潜在的巨大开销。常见的会产生子进程的语法有是 eval 和 Command substitution,在编写 .zshrc 时应该尽量避免使用它们。

例如,Homebrew 是通过 Ruby —— 一种没有性能优势的语言编写的,而且 Homebrew 的开发者甚至因为不会翻转二叉树而错失了 Google 的 offer(想必大家大体可以猜得出 Homebrew 中的负优化),因此在 zsh 启动时产生一个子进程运行 Homebrew 将是不能忍受的,绝大部分使用 Homebrew 的人都不会改变 Homebrew 的路径,因此与其在 .zshrc 中使用 $(brew --prefix),不如直接将命令执行的结果(/usr/local)直接写在 .zshrc 中。

启用 ZSH_DISABLE_COMPFIX

oh my zsh 内置了安全功能、避免 oh my zsh 插件使用不安全的目录和文件,但是这意味着插件在加载时需要通过一系列 security checker。通过禁用安全功能 (export ZSH_DISABLE_COMPFIX="true")可以使 zsh 启动速度加快 0.06s。微不足道,但值得一试。

7. 针对 macOS 的优化

path_helper

和 Linux 不同,在 macOS 上 zsh 启动序列的第一项为 /etc/zprofile 而不是 ~/.zprofile。macOS 通过 /etc/zprofile 来调用 path_helper

$ cat /etc/zprofile



if [ -x /usr/libexec/path_helper ]; then
  eval `/usr/libexec/path_helper -s`
fi

if [ "${BASH-no}" != "no" ]; then
  [ -r /etc/bashrc ] && . /etc/bashrc
fi

而 path_helper 又会读取 /etc/paths 、/etc/paths/d、etc/manpaths 和 etc/manpaths.d、并将其添加到 PATH 和 MANPATH 变量中。通过 path_helper macOS 提供了一种快速在不同 shell 中共享 PATH 和 MANPATH 的方法。过去,path_helper 是一个 运行速度很慢的 shell 脚本[6] 以至于有人制作了 专门的 patch[7]、甚至 使用 Perl[8] 重写了一个替代品。不过 macOS 意识到了这个问题,现在 path_helper 不再是一个脚本而是一个预编译好的二进制文件。如果你通过 profiling 发现 path_helper 有在拖累 zsh 启动,那么可以考虑放弃使用 /etc/paths/d、而是在 .zshrc 中直接维护

login process

默认在启动、终端登陆 shell 时会触发 macOS 的 login -fp username。这一操作会调用 syslog() 函数向 /var/log/asl 写入日志、并读取上一次登录记录、以 Last login 的形式显示出来。你可以使用下述命令证实这一行为:

ps -ef | grep login

如果想要通过减少日志写入来加快 zsh 启动速度,可以修改 etc/asl.conf 配置文件中定义的日志等级。

不少文章也提到,修改 iTerm2 设置中的 Login Command/bin/zsh 可以加快 zsh 启动速度,本质上也是绕过了上述读取和写入日志的环节。

ASL 即 Apple System Log,macOS 10.12 起被弃用,但是仍有系统组件在使用这一接口。

8. 尾声

经过一系列优化,我终于让 zsh 启动速度提升了十倍,速度甚至不亚于 fish 等以性能著称的 shell:

$ for i in $(seq 1 5); do /usr/bin/time /bin/zsh -i -c exit; done

        0.14 real         0.08 user         0.05 sys
        0.12 real         0.07 user         0.04 sys
        0.12 real         0.07 user         0.04 sys
        0.13 real         0.07 user         0.04 sys
        0.13 real         0.07 user         0.04 sys

如果对我的 .zshrc 文件感兴趣,可以 前往 GitHub 查看我开源的 dotfiles[9]

原文链接:https://blog.skk.moe/post/make-oh-my-zsh-fly/

参考资料

[1]

kcachegrind: http://kcachegrind.sourceforge.net/html/Home.html

[2]

网页的图片可以 lazyload: https://blog.skk.moe/post/img-lazyload-hexo/

[3]

Disqus 评论系统可以 lazyload: https://blog.skk.moe/post/prevent-disqus-from-slowing-your-site/

[4]

Nali CLI: https://nali.skk.moe/

[5]

tj/n: https://github.com/tj/n

[6]

运行速度很慢的 shell 脚本: https://mjtsai.com/blog/2009/04/01/slow-opening-terminal-windows

[7]

专门的 patch: https://gist.github.com/mkhl/123525

[8]

使用 Perl: https://github.com/mgprot/path_helper

[9]

前往 GitHub 查看我开源的 dotfiles: https://github.com/SukkaW/dotfiles/blob/master/_zshrc/macos.zshrc

文章分享自微信公众号:
云原生实验室

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

原始发表时间:2020-08-11
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • ZSH 自动读取 macOS 系统代理配置并设置环境变量

    和其它 Linux 的 DE 一样,macOS 也支持在“系统偏好设置”中设置 HTTP 代理、HTTPS 代理,但是 macOS 并不会在终端(Termina...

    米开朗基杨
  • 推荐一款 macOS 终端下自动配置系统代理的神器 ZSH-OSX-AutoProxy

    和其它 Linux 的 DE 一样,macOS 也支持在“系统偏好设置”中设置 HTTP 代理、HTTPS 代理,但是 macOS 并不会在终端(Termina...

    iMike
  • Linux/Mac通过Oh-my-zsh配置Zsh插件,让你的终端更加强大且智能

    几个月前,我们就介绍了ZSH(z shell),并介绍配置了Oh-my-zsh:Linux/Mac如何配置ZSH并使用Oh-my-zsh?让你的终端更加实用、美...

    Mintimate
  • 再见 XShell 和 ITerm 2,是时候拥抱全平台高颜值终端工具 Hyper 了!

    不论是 macOS 还是 Windows 下,我们都不推荐使用系统自带终端。无论是可拓展性还是可编程性都被「系统自带」这样的特点限制。特别是 Windows 下...

    iMike
  • 安装完Ubuntu 18.04之后要做的几件事

    版权声明:本文为博主原创文章,转载请注明出处。 ...

    乐百川
  • Linux/Mac如何配置ZSH并使用Oh-my-zsh?让你的终端更加实用、美观

    现在,越来越多的人趋向使用ZSH取代(Linux)原本的Bash作为自己的终端Shell。的确,ZSH才是适用于现代的Shell:

    Mintimate
  • 使用 ohmyzsh 打造 windows、ubuntu、mac 系统高效终端命令行工具

    搜索启用或关闭 windows 功能,勾选适用于 Linux 的 Windows 子系统,确定后重启电脑。

    若川
  • 买个腾讯云服务器玩玩

    在 CentOS/RHEL 和 Fedora 系统中允许 wheel 组中的用户执行所有的命令。使用 usermod 命令将用户 vivek 添加到 wheel...

    东风微鸣
  • Zsh和Oh My Zsh的安装配置

    能偷懒就偷点懒,我开始用zsh了,主要是看上了自动补全功能。。一直我都不怎么敢用,因为有时候可能一条命令错了就没办法挽回了。。然后我找了一下除了等下要装的自动补...

    砸漏
  • antigen简介

    版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/...

    乐百川
  • 终端配置zsh shell

    一直以来我在 Linux 终端用的 shell 都是 zsh,相比默认的 bash 来说,配合上插件的 zsh 功能更加强大并且十分美观,今天刚好要在服务器上重...

    棒棒鸡不棒
  • 让你提前体验 macOS Catalina 的 Shell — Oh My Zsh 配置指南

    在月初召开的 WWDC 2019 上,Apple 公布了下一代 macOS — macOS Catalina。除了全新的音乐和电视等 app、支持 iPad ...

    iMike
  • Manjaro安装配置美化记录

    记录自己Manjaro18安装的一些坑,避免下次满互联网找解决方法。在此之前试过Manjaro、Ubuntu、Fedora、linux Mint系统的pac、y...

    爱写bug
  • 推荐 18 个终端命令行工具,说实话我心动了

    终端是程序员的必备工具之一,本文将介绍许多牛逼且实用的开源工具,本文仅对工具做基本介绍,不提供安装方法,因为这些工具的安装方法在项目的 github 首页上基本...

    猿天地
  • 这篇 iTerm2 + Oh My Zsh 教程手把手让你成为这条街最靓的仔

    作为一名程序员,开发环境不舒服会很大程度影响开发效率,所以一定要花时间好好整一下开发环境(好了,我知道你是在给摸鱼找借口)。

    桃翁
  • Mac下iTerm2配合zsh食用 体验Up!

    Mac下默认的bash终端使用久了,感觉很多地方都不方便,所以就准备安装zsh,听说跟oh my zsh更配哟!

    零式的天空
  • 如何配置一个高效、漂亮、爱不释手的终端?

    程序员和电脑进行交互最多的场合就是 terminal 了,这也是一个高频要素,如果有一个好用且好看的 terminal,那会直接改善你的生活质量。本文分享如何配...

    somenzz
  • 打造 Mac 下高颜值好用的终端环境

    最近有很多朋友看了我的文章之后,问我你终端是怎么设置的,为什么如此炫酷,这这这...让我怎么说,难道我的文章不干吗?还是特干看不下去了?好吧,今天趁着周末给大家...

    公众号: 云原生生态圈
  • 推荐 18 个终端命令行工具

    终端是程序员的必备工具之一,本文将介绍许多牛逼且实用的开源工具,本文仅对工具做基本介绍,不提供安装方法,因为这些工具的安装方法在项目的 github 首页上基本...

    终码一生

扫码关注腾讯云开发者

领取腾讯云代金券