Optymalizacja .bashrc — jak zmierzyć i przyspieszyć start Basha

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 -c jest 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 ~/.bashrc

Pomiar 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 -20

Trace 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:

  1. Cache — wymaga przechowywania stanu w głównej powłoce i sensownego odświeżania. Samo PWD nie wystarczy: branch może się zmienić bez cd. Zmienne ustawione w $(...) nie wrócą do rodzica.
  2. Asynchroniczny prompt — wymaga mechanizmu przekazania wyniku z procesu w tle i odświeżenia promptu. Samo dodanie & nie rozwiązuje tego problemu.
  3. Statyczny PS1 + komenda gs — usuń git z PS1 całkowicie, dodaj alias gs='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ć

PlikCo tam wkładaćCzego unikać
~/.bash_profileZmienne środowiskowe (PATH, EDITOR, LANG), source .bashrcAliasy, funkcje, completions
~/.bashrcAliasy, funkcje, prompt, completions (cached), lazy loadersEager init managerów wersji, sync git w PS1
~/.bashrc.localMaszyno-specyficzne (work laptop, klastry), nie commitowane do dotfilesKonfiguracja przenośna
~/.inputrcReadline: wyszukiwanie historii i skróty klawiszowe; konfiguracja wczytywana przez korzystające z Readline programyWszystko 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 .bashrc w skryptach — wydziel potrzebne funkcje do osobnego pliku. shopt -s expand_aliases tylko 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_aliases i source pojedynczy plik. 50 aliasów to 50 wywołań builtina alias — 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ę przez exit. 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.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry