Đời quá ngắn để dùng một terminal chậm chạp
(mijndertstuij.nl)- Tốc độ của terminal dùng cả ngày ảnh hưởng trực tiếp đến hiệu suất làm việc; những độ trễ nhỏ khi mở tab mới, gõ phím và tự động hoàn thành nếu lặp lại hàng trăm lần mỗi ngày sẽ trở nên kém hiệu quả
- Một interactive shell đã tải đầy đủ, gồm tự động hoàn thành, tô sáng cú pháp, gợi ý tự động, fzf và direnv, nay khởi động chỉ trong khoảng 30 mili giây, còn tab mới thì mở gần như tức thì
- Bí quyết lớn nhất là không dùng framework hay plugin manager như oh-my-zsh hoặc prezto; chỉ
git clonetrực tiếp 3 plugin rồisourcechúng trong.zshrc - Dùng cache
compinit, lazy-loading, prompt bất đồng bộ, terminal tăng tốc bằng GPU và các kỹ thuật khác để giảm tối đa độ trễ khi khởi động, hiển thị prompt và nhập liệu - Phần lớn tối ưu hóa không phải là thêm thứ gì đó mà là loại bỏ những gì không cần thiết; cốt lõi là chỉ chủ đích thêm vào những gì thực sự hay dùng
Vì sao cần một terminal nhanh
- Gần như mọi công việc đều diễn ra trong terminal, dùng Git,
kubectl,tmux, kết nốisshtới server suốt cả ngày - Công cụ dùng thường xuyên như vậy cần phải nhanh, và độ trễ khi mở tab mới, nhập ký tự hay bấm Tab để tự động hoàn thành sẽ được cảm nhận hàng trăm lần mỗi ngày
- Sự tích lũy của những độ trễ nhỏ này giống như death by a thousand cuts
Kết quả đo tốc độ khởi động shell
- Sau khi tối ưu, shell khởi động trong khoảng 30 mili giây; lệnh đo dùng là
for i in {1..5}; do /usr/bin/time zsh -i -c exit; done - Một interactive shell đầy đủ với tự động hoàn thành, tô sáng cú pháp, gợi ý tự động, fzf và direnv được tải trong thời gian ngắn hơn một frame đơn ở 30fps
- Đây không phải kết quả của một dự án tối ưu hóa lớn, mà là hệ quả của thói quen giữ cho shell tối giản và nhanh trong nhiều năm
- Toàn bộ cấu hình được công khai trong kho dotfiles
Không dùng framework
- Lợi ích lớn nhất đến từ những thứ không tồn tại: không dùng oh-my-zsh, prezto hay plugin manager
- Nếu chỉ dùng khoảng 5% trong số hàng trăm plugin và theme của oh-my-zsh, thì mỗi lần mở shell bạn vẫn phải trả chi phí thời gian và tài nguyên tính toán cho 95% còn lại
- Plugin manager lại còn chồng thêm overhead lên trên đó
- Chỉ dùng đúng 3 plugin, được script cài đặt
git clonemột lần rồisourcetrong.zshrcfzf-tab,zsh-autosuggestions,zsh-syntax-highlighting- Không có plugin manager nào phải phân giải phụ thuộc lúc khởi động; việc
sourcecác file đã có sẵn trên đĩa gần như không tốn chi phí
Cache tự động hoàn thành
compinitlà một trong những tác vụ tốn kém nhất trong.zshrcđiển hình, vì mặc định nó sẽ thực hiện kiểm tra bảo mật cho mọi file tự động hoàn thành mỗi lần mở shell- Cách giải quyết là chỉ chạy đầy đủ khi cache (
.zcompdump) đã cũ hơn 24 giờ; còn lại thì dùng-Cđể bỏ qua kiểm tra- Glob qualifier
#qNmh-24có nghĩa là "tồn tại và được sửa đổi trong vòng 24 giờ gần đây" - Chỉ chạy
compinitđầy đủ một lần mỗi ngày, còn lại dùng bản đọc từ cache
- Glob qualifier
Lazy-loading
nvmlà một trong những nguyên nhân tai tiếng nhất làm chậm thời gian khởi động shell; nếusourcengay lúc khởi động thì rất dễ cộng thêm 0,5 giây- Không phải shell nào cũng cần
nvm; nó chỉ cần khi bạn gõnvm, nên có thể bọc nó trong một hàm tự thay thế chính nó ở lần dùng đầu tiên- Lần gọi
nvmđầu tiên sẽ xóa stub,sourcenvm thật (đồng thời dùng--no-useđể tránh cả việc phân giải phiên bản node), rồi chuyển tiếp đối số
- Lần gọi
- Tự động hoàn thành của
kubectlcũng làm tương tự: vì nó gọi binarykubectlđể sinh script tự động hoàn thành, nên chỉ nạp sau lần chạy đầu tiên - Mọi công cụ bảo bạn thêm
eval "$(tool init zsh)"vào.zshrcđều sẽ fork process khi khởi động và evaluate đầu ra của nó, nên đều là ứng viên cho lazy-loading direnvvàfzfthì nhanh và được dùng thường xuyên, nên vẫn được nạp ngay; cần đánh giá thật nghiêm khắc thứ gì mới là thứ bạn thực sự dùng nhiều
Prompt không chặn
- Prompt chạy
git statusđồng bộ sẽ bị chậm ở các repository hơi lớn, và vì điều đó xảy ra mỗi lần nhấn Enter nên còn tệ hơn cả khởi động chậm - Tác giả dùng pure, thứ hiển thị prompt ngay lập tức và sẽ điền thông tin git bất đồng bộ khi sẵn sàng
- Đã từng thử thay bằng
vcs_infotích hợp sẵn trong zsh, nhưng cách làm bất đồng bộ của pure vẫn tốt hơn - Bạn cũng có thể tự triển khai
git statusbất đồng bộ trong prompt, nhưng pure đã bọc sẵn việc đó khá tốt
Bản thân terminal emulator
- Khởi động shell mới chỉ là một nửa câu chuyện; chính terminal emulator cũng có thể thêm độ trễ nhập liệu
- Tác giả dùng Ghostty, một terminal native tăng tốc bằng GPU, với cấu hình chỉ 7 dòng
- Kết hợp với alias
tmux new -A -s main(t), cửa sổ terminal mới sẽ quay lại ngay phiên làm việc hiện có
Cách tự đo hiệu năng shell của bạn
- Bạn có thể tự đo terminal của mình để biết thời gian đang bị tiêu ở đâu; có 3 loại độ trễ cần kiểm tra là thời gian khởi động, độ trễ prompt và độ trễ nhập liệu
- Cách đo cơ bản là chạy
time zsh -i -c exitvài lần; lần đầu luôn chậm hơn do cold cache- Dưới 100ms là ổn, dưới 50ms là rất tốt, còn trên 500ms là có chỗ cần chỉnh
- Dùng hyperfine để có thống kê chính xác hơn:
hyperfine --warmup 3 'zsh -i -c exit' - Tận dụng profiler tích hợp sẵn của zsh
- Thêm
zmodload zsh/zprofở đầu.zshrcvàzprofở cuối để in ra bảng sắp xếp theo thời gian tiêu tốn - Các mục đứng đầu thường là
compinit,source nvm.sh, vàeval "$(...)"; hãy sửa từ mục trên cùng rồi lặp lại việc chạy đo - Xong thì xóa hai dòng đó đi
- Thêm
- Nếu zprof vẫn chưa đủ, có thể theo dõi toàn bộ quá trình khởi động bằng timestamp:
zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20- Hoặc đặt
PS4='+%D{%s.%6.}: 'rồi chạyzsh -ixc exit 2> startup.logđể tìm các bước nhảy lớn giữa các dòng
- Hoặc đặt
- Có khi khởi động đã nhanh nhưng redraw của prompt vẫn chậm; hãy
cdvào repository Git lớn nhất rồi nhấn Enter, nếu thấy có độ trễ trước khi prompt tiếp theo hiện ra thì prompt đang làm việc đồng bộ- Khi đó có thể chuyển sang prompt bất đồng bộ hoặc bỏ bớt tính năng Git
Kết luận
- Phần lớn tối ưu hóa là chuyện loại bỏ bớt; điều quan trọng là hành động có chủ đích và chỉ thêm vào những gì bạn thực sự sẽ dùng
- Làm vậy thì hàng chục session bạn mở mỗi ngày đều sẽ mở tức thì, và terminal sẽ giống một phần mở rộng của bộ não hơn là một ứng dụng bắt bạn phải chờ
- Với công cụ dùng suốt cả ngày, tốc độ này là điều không thể thỏa hiệp
- Toàn bộ cấu hình trên đều được công khai trong kho dotfiles
1 bình luận
Ý kiến trên Lobste.rs
Nói chính xác thì phần lớn ở đây không phải terminal mà là shell
Tốt hơn là dùng công cụ có mặc định hợp lý, vì thế cứ dùng fish là được
Tôi thích việc các tính năng kiểu tab completion hiện đại có thể chọn bằng phím mũi tên đã có sẵn theo mặc định; trên máy cá nhân tôi vẫn dùng ZSH, nhưng đó là vì chưa có thời gian chỉnh lại cấu hình Nix và home manager
Sẽ rất tuyệt nếu có một shell với mặc định hợp lý và completion tích hợp nhanh, mà không cần vứt bỏ hay viết lại các công cụ dựa trên bash
Thỉnh thoảng tôi tự hỏi những thứ như prompt không chặn hay terminal dùng OpenGL có thật sự đáng giá hơn việc chỉ dùng
PS1="\\W: "trong xterm hay khôngThêm vào đó nó rất nhanh và có ưu thế là “chuẩn”, nên những lỗi còn lại đa phần cũng nhỏ hoặc các chương trình chạy trong đó có thể sẽ coi đó là hành vi bình thường
Vì vậy tôi quay lại dùng xterm
Khởi động zsh vốn dĩ rất nhanh, nó chỉ chậm đi khi người dùng tự làm nó chậm
Chỉ cần đừng nhét vào đó cả đống thứ mình không hiểu, bao gồm cả các thư viện tự xưng là “tối giản” nhưng lại chạy hàng trăm lệnh mỗi lần dựng prompt
Cấu hình zsh của tôi là vài trăm dòng, tiến hóa cực kỳ chậm từ thập niên 90 đến nay; tôi hiểu mọi dòng và biết vì sao nó tồn tại
Tôi chưa từng cố tối ưu đặc biệt cho tốc độ, nhưng nó vẫn khởi động trong 20ms, và nếu tôi thêm thay đổi ngớ ngẩn nào khiến nó chậm đi thì sẽ nhận ra ngay và sửa được
Tôi không thích việc các benchmark hỏng kiểu
time zsh -i -c exitvẫn còn được dùng phổ biếnNó đo hoàn toàn sai thứ cần đo, và một số trình quản lý plugin zsh thậm chí còn tối ưu cho chỉ số vô dụng này bằng cách hy sinh độ trễ khởi động shell thực tế
zsh-bench có một mục giải thích vì sao benchmark này vô nghĩa: https://github.com/romkatv/zsh-bench#how-not-to-benchmark
Những chỉ số như độ trễ tới prompt đầu tiên hay độ trễ nhập liệu mà zsh-bench đo được hữu ích hơn nhiều
Tôi cứ tưởng đây sẽ là câu chuyện về bug của terminal tăng tốc bằng GPU, nên khá vui khi không phải vậy
Caching completion là một mẹo hay, và tôi đang dùng zsh trên máy Mac của công ty, nơi chỉ cần nghĩ đến việc mở tab mới là con quay cầu vồng đã hiện ra, nên hy vọng nó sẽ giúp ích
Với completion của kubectl, tôi tò mò không biết phần chậm là do sinh completion hay do nạp nó vào, và nếu là vế đầu thì liệu lưu ra file rồi nạp lại có giúp giảm thời gian khởi động không
Tôi làm vậy với
jj, và khi chuyển sangjjthì cũng bỏ luôn prompt chạygit statusTôi hơi tiếc là tác giả không ghi cả thời gian của mình, vì như vậy tôi sẽ biết 0,287 giây của tôi là trung bình hay chậm
Sau đó đo lại thì
.bashrcgần như trống là 0,007 giây, sau skim key binding là 0,043 giây, sau mise là 0,115 giây, sau completion của jj là 0,186 giây, và nếu đọc cả/etc/bashrcthì là 0,294 giây, nên có vẻ vẫn còn chỗ để cải thiệntime shell -c exitthì ra khoảng 50msĐiều làm tôi khó chịu nhất khi dùng môi trường Linux của người khác là những animation vô nghĩa khắp nơi
Trên máy tôi, bấm phím tắt là cửa sổ terminal gần như mở ra ngay lập tức, thỉnh thoảng chỉ thấy một cái chớp rất ngắn giữa cửa sổ và prompt
Vì vậy bài test end-to-end toàn bộ, tức mở cửa sổ mới, làm gì đó trong shell rồi đóng lại, mới là quan trọng; và khi chạy
time mytermrồi bấm Ctrl+D trong cửa sổ để đóng, nó luôn dưới 0,120 giâyKhi bỏ các animation và compositing vô ích đi, rất nhiều thứ trở nên khả thi; kể cả khi so sánh chênh lệch giữa hai bảng tính, tôi chỉ việc phóng to hai cửa sổ rồi dùng phím tắt cuộn cửa sổ lên xuống để đổi qua lại thật nhanh, sự khác biệt hiện ra ngay
Làm cùng việc đó trên Windows với animation của Excel thì quá mất tập trung
Ngay cả với cấu hình trống,
zsh -i -c exitcũng trung bình 129,8ms, còn toàn bộ cấu hình thì mất khoảng 250ms, khá tương tựTôi có giảm được trung bình khoảng 5ms nhờ compinit caching, nhưng vì có thể bị thiếu completion nên tôi thấy công sức bỏ ra không đáng lắm
Gần đây khởi động zsh chậm đến mức gần như treo, và dù chưa xác định chính xác nguyên nhân, tôi đã xác nhận rằng compinit chiếm phần lớn đường găng
Tôi triển khai caching gần như giống hệt cách bài viết đề xuất và đã loại bỏ được tình trạng chậm đó; đồng thời thấy glob qualifier rất hay nên cảm giác mình cũng nên cải thiện cách làm
Trước đây tôi còn không biết có thể làm thế, và thành thật mà nói nó trông hơi đáng ngờ, nhưng tôi vẫn sẽ dùng
Trước đó, khi tạo đường dẫn đích, tôi dùng cách khá thô là
date -IdTôi thích những công cụ được cấu hình bằng một ngôn ngữ lập trình hoàn chỉnh như zsh, vì có thể tự triển khai tính năng như caching mà không cần chờ tác giả thêm vào
Tôi dùng zsh gần 20 năm mà chưa từng dùng framework hay plugin manager nào, và có vẻ mấy thứ đó chủ yếu được dùng vì mục đích tạo kiểu
May cho tôi là tôi không quan tâm đến thẩm mỹ của môi trường máy tính; prompt tự viết của tôi cũng rất cơ bản, nhỏ gọn, có tính thông tin nhưng hoàn toàn không hào nhoáng, và tôi dùng theme terminal mặc định nền đen
Nhiều instance shell có thể làm cùng một việc song song, và tôi thường gặp chuyện đó khi bật các instance song song để thực hành trong tmux
Ngoài ra còn có thể chia sẻ thư mục home giữa nhiều host, nhất là container, nên cuối cùng tôi dọn lại mọi thứ theo cách có cả file khóa, kiểm tra hết hạn và xử lý điều kiện cho
zcompileTiếc là cấu hình fish của tôi dường như cũng dần trôi theo cùng hướng đó, nên dự định lúc rảnh vào thứ Hai sẽ profiling thử để xem kỹ thuật lazy loading có thực sự hữu ích trong trường hợp của tôi không
Phần lớn thời gian chậm có lẽ là do module git của Starship, nhưng cũng có khá nhiều alias và hàm trợ giúp có thể lazy load
Trong Emacs, từ lâu tôi đã khởi tạo sẵn một shell staging ở nền
Mở terminal nghĩa là mở một cửa sổ mới vào buffer đó và đổi tên nó, rồi fork một thread để chuẩn bị lại shell cho lần tiếp theo
Vì vậy không có độ trễ khởi động
Tôi nhớ trước đây từng cố gò ra một giải pháp ngoài Emacs bằng reptyr, nhưng cuối cùng không tiếp tục dùng hướng đó nữa, dù giờ cũng không nhớ rõ lý do
https://github.com/nelhage/reptyr
Khi xem xét tương tự, tôi phát hiện
zsh-abbrngốn khoảng 100ms thời gian khởi động, nhưng mức đó vẫn chấp nhận đượcCó thể cắt 10ms ở chỗ này chỗ kia, nhưng xét đến tính năng bị mất thì có vẻ không đáng
Tôi sẽ sống chung với thời gian khởi động khoảng 300ms; như vậy là đủ nhanh, và tôi cũng hiếm khi mở terminal liên tục hay cần gõ ngay lập tức
Dù vậy bài viết rất hay, tôi biết thêm về
hyperfinevà đã nhìn lại vài file khởi động của zshNhờ đó mà tôi cuối cùng cũng sửa zshrc bị trì hoãn từ lâu, và giờ xuống được 80ms nên rất hài lòng
Đời tôi đủ dài để chịu đựng terminal chậm, và đôi khi tôi còn ước terminal chậm hơn một chút
Ví dụ, nếu trên root console có độ trễ mặc định 5 giây trước khi thực sự chạy lệnh để còn kịp nhấn Ctrl+C hủy lỗi gõ nhầm, có lẽ tôi đã tiết kiệm được vài ngày trong quãng tuổi trẻ nổi loạn của mình