Dlaczego twój terminal startuje 800 milisekund
Otwierasz nową kartę terminala i czekasz na prompt. W .bashrc startują nvm, pyenv, completion dla kubectl i integracja z Gitem — nawet jeśli chciałeś tylko sprawdzić jeden plik. Zanim zmienisz powłokę, sprawdź, który z tych elementów zabiera czas.
Przejdziemy przez pliki startowe Basha, pomiar i odraczanie inicjalizacji do pierwszego użycia narzędzia. Cel: krótszy start bez zgadywania, co go spowalnia. Konkretnego wyniku w milisekundach nie da się obiecać dla każdego zestawu dotfiles.
Klasyfikacja powłok — bez tego cała reszta nie ma sensu
Bash rozróżnia dwie ortogonalne osie, których kombinacja determinuje który plik konfiguracyjny zostanie wczytany:
- Login shell vs non-login shell — login to sesja inicjująca (logowanie do TTY,
ssh,su -); non-login to subshell odpalony w istniejącej sesji (nowa karta w GNOME Terminal w domyślnej konfiguracji). - Interactive vs non-interactive — Bash uruchomiony bez skryptu i bez
-cjest zwykle interaktywny, gdy stdin i stderr są terminalami. Tryb można też wymusić przez-i. Skrypt domyślnie działa nieinteraktywnie.
Kombinacja tych dwóch osi daje cztery klasy startu, każda z innym łańcuchem ładowanych plików — dokładną kolejność precyzuje dokumentacja GNU Bash o plikach startowych:
| Typ powłoki | Ładowane pliki (w kolejności) |
|---|---|
| Login + Interactive (ssh, TTY) | /etc/profile → pierwszy czytelny z: ~/.bash_profile, ~/.bash_login, ~/.profile |
| Non-login + Interactive (nowa karta) | ~/.bashrc; systemowy bashrc zależy od dystrybucji i konfiguracji |
| Non-interactive (skrypt) | Odczyt i wykonanie pliku wskazanego przez BASH_ENV, jeśli zmienna jest ustawiona |
Login + Non-interactive (cron z bash -l) | /etc/profile → pierwszy czytelny z: ~/.bash_profile, ~/.bash_login, ~/.profile; potem plik wskazany przez BASH_ENV |
Większość ludzi traktuje ~/.bashrc i ~/.bash_profile jak synonimy — błąd. Klasyczny wzorzec to source’owanie .bashrc z .bash_profile, żeby login shell też dostał konfigurację:
Tabela dotyczy zwykłego wywołania programu bash. Tryb sh, opcje startowe oraz wykrycie zdalnego uruchomienia przez sshd mogą zmienić te reguły. Nie każdy ssh host polecenie uruchamia powłokę logowania.
# ~/.bash_profile
[[ -f ~/.bashrc ]] && source ~/.bashrcPomiar startu shella — bez zgadywania
Pierwszy krok przed optymalizacją: pomiar. Bez baseline’u optymalizacja jest religią, nie inżynierią.
# Baseline: dziesięć osobnych czasów startu powłoki interaktywnej
$ for i in {1..10}; do time bash -i -c exit; done 2>&1 |
grep real | awk '{print $2}'
# Przykładowy output:
# 0m0.847s
# 0m0.832s
# 0m0.851s
# 0m0.839s
# ...Ta wartość to nasza prawda referencyjna. Teraz lokalizujemy bottleneck przez per-line tracing z timestampami:
# Bash 5+: ustaw PS4 wewnątrz powłoki, przed wczytaniem ~/.bashrc.
trace_rc=$(mktemp)
cat > "$trace_rc" <<'BASHRC'
PS4='+ ${EPOCHREALTIME} '
set -x
source "$HOME/.bashrc"
BASHRC
bash --noprofile --rcfile "$trace_rc" -i -c exit 2> /tmp/bash-trace.log
rm -f -- "$trace_rc"
# Odstęp przypisujemy poprzedniej linii trace, nie następnej.
LC_ALL=C awk '
/^\++ [0-9]+\.[0-9]+ / {
if (seen) {
delta = $2 - prev
if (delta > 0.010) printf "%.3fs %s\n", delta, line
}
prev = $2; line = $0; seen = 1
}
' /tmp/bash-trace.log | sort -rn | head -20Trace obejmuje tutaj twój ~/.bashrc, nie cały start powłoki ani systemowy bashrc. Wskazuje kandydatów do sprawdzenia, ale sam dodaje narzut, a zagnieżdżone wywołania utrudniają przypisanie czasu do jednej funkcji. Potwierdź podejrzenie, wyłączając dany fragment i powtarzając pomiar bez tracingu.
Najgorsi przestępcy — patterns które kradną sekundy
Sprawdź przede wszystkim inicjalizację managerów wersji, generowanie completions i funkcje promptu. Koszt zależy od wersji narzędzi, konfiguracji i repozytorium.
Jeśli w wielu sesjach nie używasz danego narzędzia, płacenie za jego inicjalizację przy każdym starcie jest dobrym kandydatem do optymalizacji. Jeśli używasz go od razu — lazy loading przede wszystkim przesunie oczekiwanie.
Lazy loading — odroczenie kosztu do faktycznego użycia
Wzorzec lazy loading polega na zarejestrowaniu stub funkcji pod nazwą binarki, która przy pierwszym wywołaniu inicjalizuje pełne środowisko i podmienia samą siebie:
# ~/.bashrc — lazy nvm loader
# Koszt inicjalizacji przeniesiony na pierwsze użycie
# Katalog instalacji — bez ładowania nvm.sh
export NVM_DIR="$HOME/.nvm"
# Stub funkcje dla każdego entry pointu nvm
_lazy_load_nvm() {
# Usuń stub'y, żeby uniknąć rekurencji
unset -f nvm node npm npx yarn pnpm 2>/dev/null
# Faktyczna inicjalizacja — koszt płacony tylko raz, w razie potrzeby
[[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh"
[[ -s "$NVM_DIR/bash_completion" ]] && source "$NVM_DIR/bash_completion"
}
nvm() { _lazy_load_nvm; nvm "$@"; }
node() { _lazy_load_nvm; node "$@"; }
npm() { _lazy_load_nvm; npm "$@"; }
npx() { _lazy_load_nvm; npx "$@"; }
yarn() { _lazy_load_nvm; yarn "$@"; }
pnpm() { _lazy_load_nvm; pnpm "$@"; }Analogicznie dla pyenv:
Poniższy wariant zakłada zainstalowane pyenv i opcjonalny plugin pyenv-virtualenv. Jeśli nie używasz pluginu, pomiń virtualenv-init. Sprawdź również aktywację środowisk i completions: odroczenie inicjalizacji może zmienić ich dostępność przed pierwszym użyciem.
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
_lazy_load_pyenv() {
unset -f pyenv python python3 pip pip3 2>/dev/null
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)" 2>/dev/null
}
pyenv() { _lazy_load_pyenv; pyenv "$@"; }
python() { _lazy_load_pyenv; python "$@"; }
python3() { _lazy_load_pyenv; python3 "$@"; }
pip() { _lazy_load_pyenv; pip "$@"; }
pip3() { _lazy_load_pyenv; pip3 "$@"; }Koszt płacisz raz, w pierwszym wywołaniu — nie w każdym otwarciu karty.
Completion files — preload zamiast eval
Drugi pożeracz czasu to generowanie completion files przez eval "$(kubectl completion bash)". Każde otwarcie shella spawnuje proces kubectl, parsuje jego output, evaluje go w bieżącej powłoce. Lepszy wzorzec: cache na dysku:
# Wykonaj po instalacji lub aktualizacji kubectl:
mkdir -p "$HOME/.cache"
kubectl completion bash > "$HOME/.cache/kubectl-completion.bash.tmp" &&
mv "$HOME/.cache/kubectl-completion.bash.tmp" "$HOME/.cache/kubectl-completion.bash"
# W ~/.bashrc pozostaw tylko:
[[ -r "$HOME/.cache/kubectl-completion.bash" ]] &&
source "$HOME/.cache/kubectl-completion.bash"Po aktualizacji kubectl wygeneruj plik ponownie. Start powłoki nadal odczytuje i wykonuje zapisany skrypt, ale nie uruchamia kubectl, żeby go odtworzyć.
PS1 — koszt płacony w każdym Enterze
Optymalizacja startu jest jednorazowa. Optymalizacja PS1 dotyczy każdego naciśnięcia Enter. Klasyczny problem:
# __git_ps1 musi być wcześniej zdefiniowane przez integrację z Gitem.
PS1='\u@\h:\w$(__git_ps1 " (%s)")\$ '
# Koszt funkcji ponosisz przy każdym wyświetleniu promptu.W dużym repozytorium sprawdzanie stanu może być zauważalne. Najpierw zobacz, które informacje prompt rzeczywiście pobiera. Masz kilka możliwości:
- Cache — wymaga przechowywania stanu w głównej powłoce i sensownego odświeżania. Samo
PWDnie wystarczy: branch może się zmienić bezcd. Zmienne ustawione w$(...)nie wrócą do rodzica. - Asynchroniczny prompt — wymaga mechanizmu przekazania wyniku z procesu w tle i odświeżenia promptu. Samo dodanie
&nie rozwiązuje tego problemu. - Statyczny PS1 + komenda
gs— usuń git z PS1 całkowicie, dodaj aliasgs='git status -sb'. Brutalne, ale działa.
# Najprostszy wariant: Git na żądanie
PS1='\u@\h:\w\$ '
alias gs='git status -sb' Reorganizacja — co gdzie powinno trafić
| Plik | Co tam wkładać | Czego unikać |
|---|---|---|
~/.bash_profile | Zmienne środowiskowe (PATH, EDITOR, LANG), source .bashrc | Aliasy, funkcje, completions |
~/.bashrc | Aliasy, funkcje, prompt, completions (cached), lazy loaders | Eager init managerów wersji, sync git w PS1 |
~/.bashrc.local | Maszyno-specyficzne (work laptop, klastry), nie commitowane do dotfiles | Konfiguracja przenośna |
~/.inputrc | Readline: wyszukiwanie historii i skróty klawiszowe; konfiguracja wczytywana przez korzystające z Readline programy | Wszystko inne |
Przeniesienie kosztownej inicjalizacji z .bashrc do .bash_profile nie usuwa jej kosztu. Zmienia tylko zestaw sesji, które go poniosą.
Porównanie przed i po zmianie
Zmierz kilka uruchomień każdej konfiguracji w tych samych warunkach. Zmieniaj jeden element naraz i zachowuj pojedyncze czasy, nie tylko średnią. Oprócz startu powłoki sprawdź completions, aktywację środowiska oraz pierwsze wywołanie każdego narzędzia.
Czego nie robić — anti-patterns z prawdziwych dotfiles
- Ładowanie całego
.bashrcw skryptach — wydziel potrzebne funkcje do osobnego pliku.shopt -s expand_aliasestylko włącza rozwijanie aliasów; nie definiuje ich ani nie wczytuje konfiguracji. - Automatyczne odraczanie każdej integracji — jeśli skróty fzf są potrzebne od pierwszej komendy, pozostaw je dostępne. Mierz koszt i oceniaj zmianę w swoim sposobie pracy.
- Aliasy do
git,docker,kubectl— przenieś do statycznego pliku~/.bash_aliasesi source pojedynczy plik. 50 aliasów to 50 wywołań builtinaalias— w sumie milisekundy, ale dyscyplina. - Ignorowanie pierwszego użycia — autoload completions może przyspieszyć start kosztem pierwszego naciśnięcia Tab. Zmierz oba momenty.
- Rozbudowany prompt — sprawdź liczbę uruchamianych procesów przy użyciu strace. Uruchom
strace -f -e trace=execve -o /tmp/prompt-exec.log bash --noprofile -i, wyświetl kilka promptów i zakończ sesję przezexit. Log obejmuje też start powłoki, więc oddziel go od kolejnych odświeżeń.
Podsumowanie: shell jako narzędzie inżynierskie
Bashrc nie jest kosmetyką. To plik konfiguracyjny wykonywany dziesiątki, czasem setki razy dziennie. Optymalizacja startu shella to nie premature optimization — to amortyzacja kosztu rozłożona na każdą interakcję z systemem.
Nie ma znaczenia, czy twój prompt wygląda minimalistycznie. Znaczenie ma to, co wykonuje przy starcie i przy każdym Enterze. Usuń niepotrzebną pracę tam, gdzie pomiar pokazuje koszt.
Mierz przed optymalizacją. Mierz po. Decyzje na podstawie time, nie na podstawie tego co czytałeś w r/unixporn.
Dyscyplina „najpierw zmierz, potem optymalizuj”, która rządzi profilowaniem startu shella, to ta sama zasada, na której opiera się decyzja, kiedy event loop na epoll przestaje wystarczać, a io_uring to over-engineering. Samo namierzanie wąskiego gardła — czy to w .bashrc, czy w produkcji — jest z kolei tematem tekstu o debugowaniu jako procesie dedukcji.


