一、Hook性能的重要性
在Claude Code的Hook系统中,Hook脚本会随事件触发而执行,直接影响用户的交互体验。如果Hook执行效率低下,会导致用户等待时间显著增加,降低整体使用流畅度。特别是 before Hook(在Claude Code响应用户之前执行)对响应速度的影响最为直接。
核心概念: before Hook在Claude Code生成回复之前执行,任何延迟都会直接叠加到用户等待时间上;after Hook虽然不阻塞回复生成,但仍需确保快速完成以释放系统资源。
不同的Hook类型对性能的敏感度不同:
| Hook类型 | 执行时机 | 对响应速度的影响 | 性能目标 |
| before Hook | Claude Code生成回复之前 | 直接增加用户等待时间 | <200ms |
| after Hook | Claude Code生成回复之后 | 不影响主流程但占用资源 | <500ms |
| file-change Hook | 文件变更时 | 异步触发,影响较小 | <1s |
当Hook数量增多时,性能问题会进一步放大。假设配置了10个before Hook,每个耗时100ms,那么总延迟将达到1秒,这对用户体验来说是显著的劣化。因此,对Hook进行性能优化是构建大规模Hook系统的基础。
性能优化核心目标: after Hook <500ms, before Hook <200ms。在保证功能完整的前提下,通过缓存、懒加载、异步等手段最大限度减少Hook对主流程的影响。
二、Hook执行耗时分析
性能优化的第一步是测量现状。"没有度量就没有优化"——只有清楚地了解每个Hook的执行耗时,才能找到瓶颈并有针对性地优化。以下是常用的Hook执行耗时分析方法。
2.1 使用time命令测量Hook执行时间
在Shell脚本中,可以直接使用 time 命令来测量单个Hook脚本的执行耗时:
#!/bin/bash
# 测量Hook执行耗时
# 记录开始时间
start_time=$(date +%s%N)
# ---------- Hook实际逻辑 ----------
# 检查项目状态
if [ -f "package.json" ]; then
echo "检测到Node.js项目"
fi
# 检查Git状态
if git rev-parse --git-dir > /dev/null 2>&1; then
echo "检测到Git仓库"
fi
# ---------------------------------
# 计算耗时(毫秒)
end_time=$(date +%s%N)
elapsed_ms=$(( (end_time - start_time) / 1000000 ))
echo "Hook执行完成,耗时:${elapsed_ms}ms"
# 如果超过阈值,输出警告
if [ "$elapsed_ms" -gt 200 ]; then
echo "[警告] Hook执行时间超过200ms阈值!"
fi
2.2 自动记录每个Hook的执行耗时
可以在Hook系统中集成自动耗时记录机制。通过封装一个公共的执行函数,让所有Hook都通过该函数运行,自动完成计时和日志记录:
#!/bin/bash
# 公共Hook执行封装函数
# 自动记录耗时并写入日志
HOOK_LOG_DIR="${HOME}/.claude/hooks/logs"
mkdir -p "$HOOK_LOG_DIR"
run_hook() {
local hook_name="$1"
shift
local start_time=$(date +%s%N)
# 执行Hook逻辑
"$@"
local exit_code=$?
# 计算耗时
local end_time=$(date +%s%N)
local elapsed_ms=$(( (end_time - start_time) / 1000000 ))
# 记录日志
local log_file="${HOOK_LOG_DIR}/hook_perf.log"
echo "$(date '+%Y-%m-%d %H:%M:%S') | ${hook_name} | ${elapsed_ms}ms | exit=${exit_code}" >> "$log_file"
# 慢Hook告警
if [ "$elapsed_ms" -gt 500 ]; then
echo "[性能告警] Hook '${hook_name}' 执行过慢 (${elapsed_ms}ms)"
fi
return $exit_code
}
# 示例用法
# run_hook "check-git-status" ./check_git.sh
注意: 计时机制本身也会引入微小开销(约1-2ms),在测量极短(小于10ms)的Hook时需要注意区分测量误差。对于高性能场景,建议使用专门的性能分析工具进行更精确的测量。
2.3 识别慢Hook和瓶颈
通过收集日志数据,可以分析出哪些Hook是性能瓶颈。以下是一个分析日志的脚本:
#!/bin/bash
# Hook性能分析脚本
# 分析hook_perf.log并输出性能报告
LOG_FILE="${HOME}/.claude/hooks/logs/hook_perf.log"
if [ ! -f "$LOG_FILE" ]; then
echo "未找到Hook性能日志文件"
exit 1
fi
echo "========== Hook性能分析报告 =========="
echo "分析时间:$(date '+%Y-%m-%d %H:%M:%S')"
echo ""
# 按Hook名称统计
echo "--- 各Hook平均耗时排名 ---"
awk -F'|' '{print $2}' "$LOG_FILE" | sort | uniq -c | while read count name; do
avg=$(grep "|${name}|" "$LOG_FILE" | awk -F'|' '{sum+=$3; count++} END{print int(sum/count)}')
echo "${name}: 平均${avg}ms (共${count}次)"
done | sort -t: -k2 -rn
echo ""
echo "--- 最慢的10次Hook执行 ---"
awk -F'|' '{print $3 " | " $2 " | " $1}' "$LOG_FILE" | sort -rn | head -10
echo ""
echo "--- 超过阈值的Hook次数 ---"
total=$(wc -l < "$LOG_FILE")
slow=$(awk -F'|' '{if($3+0 > 200) print}' "$LOG_FILE" | wc -l)
echo "总执行次数:${total}"
echo "超过200ms次数:${slow}"
echo "慢Hook占比:$(echo "scale=2; ${slow} * 100 / ${total}" | bc)%"
2.4 性能基准和优化目标设定
建立性能基准是持续优化的重要环节。建议按照以下步骤设定性能基准:
性能基准测试流程:
1. 基线建立
- 在无任何Hook的情况下测量Claude Code响应时间
- 记录作为基线值(例如:首次响应时间 = 800ms)
2. 逐步添加Hook
- 每添加一个Hook后重新测量响应时间
- 记录每个Hook引入的额外延迟
3. 负载测试
- 模拟同时触发多个Hook的场景
- 测试并发Hook对系统整体性能的影响
4. 优化目标设定
- before Hook合计耗时:≤200ms
- after Hook合计耗时:≤500ms
- 单个Hook最大耗时:≤100ms
- 99分位响应时间:≤300ms
最佳实践: 将性能基准测试集成到CI/CD流程中,每次Hook变更后自动运行测试,确保性能不会退化。当检测到性能回退时,自动阻止变更并通知开发者。
三、缓存策略优化
缓存是减少Hook执行开销最有效的策略之一。当Hook需要重复检查相同条件时,通过缓存避免重复计算可以大幅降低耗时。缓存策略的核心是"在正确的时间缓存结果,在正确的时间失效缓存"。
3.1 检测结果缓存:相同条件下跳过重复检查
许多Hook会在相同条件下反复执行相同的检查逻辑。例如,检查当前是否在Git仓库中、检查依赖是否已安装等。这类检查的结果在一定时间范围内不会改变,非常适合使用缓存:
#!/bin/bash
# 检测结果缓存示例
# 缓存目录
CACHE_DIR="${HOME}/.claude/hooks/cache"
mkdir -p "$CACHE_DIR"
check_with_cache() {
local cache_key="$1"
local cache_file="${CACHE_DIR}/${cache_key}"
local ttl="${2:-300}" # 默认缓存有效期5分钟
# 检查缓存是否存在且未过期
if [ -f "$cache_file" ]; then
local file_age=$(($(date +%s) - $(stat -c %Y "$cache_file")))
if [ "$file_age" -lt "$ttl" ]; then
cat "$cache_file"
return 0
fi
fi
return 1 # 缓存未命中
}
set_cache() {
local cache_key="$1"
local cache_file="${CACHE_DIR}/${cache_key}"
echo "$2" > "$cache_file"
}
# 使用示例:检查Git仓库状态(带缓存)
git_repo_check() {
local result
result=$(check_with_cache "is_git_repo" 60)
if [ $? -eq 0 ]; then
echo "$result"
return
fi
# 执行实际检查
if git rev-parse --git-dir > /dev/null 2>&1; then
set_cache "is_git_repo" "yes"
echo "yes"
else
set_cache "is_git_repo" "no"
echo "no"
fi
}
3.2 缓存失效策略:文件变更时自动失效缓存
简单的时间过期策略并不总是可靠——有时需要在文件变更时立即失效缓存。以下是一种基于文件内容哈希的缓存策略:
#!/bin/bash
# 基于文件内容哈希的缓存失效策略
# 当监控的文件发生变更时,缓存自动失效
CACHE_DIR="${HOME}/.claude/hooks/cache"
HASH_CACHE="${CACHE_DIR}/file_hashes"
# 计算文件哈希值
get_file_hash() {
local file="$1"
if [ -f "$file" ]; then
md5sum "$file" | cut -d' ' -f1
fi
}
# 检查文件是否已变更
has_file_changed() {
local file="$1"
local cache_key="$2"
local hash_file="${HASH_CACHE}/${cache_key}"
mkdir -p "$(dirname "$hash_file")"
local current_hash
current_hash=$(get_file_hash "$file")
local cached_hash=""
if [ -f "$hash_file" ]; then
cached_hash=$(cat "$hash_file")
fi
if [ "$current_hash" != "$cached_hash" ]; then
# 更新缓存哈希值
echo "$current_hash" > "$hash_file"
return 0 # 文件已变更
fi
return 1 # 文件未变
}
# 使用示例:检查package.json是否变更
if has_file_changed "package.json" "package_json"; then
echo "package.json已变更,重新检查依赖..."
# 执行完整的依赖检查逻辑
else
echo "package.json未变更,使用缓存结果"
fi
3.3 使用临时文件存储缓存结果
临时文件(位于 /tmp 目录下)适合存储短期缓存数据,系统会自动清理,无需手动管理:
#!/bin/bash
# 使用临时文件作为缓存存储
# 为当前Shell会话创建唯一的缓存命名空间
CACHE_NS="hook_cache_$$"
CACHE_DIR="/tmp/${CACHE_NS}"
mkdir -p "$CACHE_DIR"
# 注册退出清理
trap 'rm -rf "$CACHE_DIR"' EXIT
# 缓存读/写函数
cache_get() {
local key="$1"
local cache_file="${CACHE_DIR}/$(echo "$key" | md5sum | cut -d' ' -f1)"
if [ -f "$cache_file" ] && [ "$(($(date +%s) - $(stat -c %Y "$cache_file")))" -lt 60 ]; then
cat "$cache_file"
return 0
fi
return 1
}
cache_set() {
local key="$1"
local value="$2"
local cache_file="${CACHE_DIR}/$(echo "$key" | md5sum | cut -d' ' -f1)"
echo "$value" > "$cache_file"
}
# 使用示例
result=$(cache_get "project_type")
if [ $? -ne 0 ]; then
# 执行耗时检测逻辑
if [ -f "package.json" ]; then
result="node"
elif [ -f "Cargo.toml" ]; then
result="rust"
elif [ -f "go.mod" ]; then
result="go"
else
result="unknown"
fi
cache_set "project_type" "$result"
fi
echo "项目类型:${result}"
3.4 缓存有效期和自动清理
合理的缓存有效期设置可以平衡性能和数据新鲜度。不同场景建议采用不同的有效期:
| 缓存场景 | 建议有效期 | 失效策略 |
| Git仓库状态 | 30-60秒 | 时间过期 |
| 项目类型检测 | 5-10分钟 | 时间过期 + 文件变更 |
| 依赖是否已安装 | 1-5分钟 | 文件变更 |
| 环境变量/配置 | 整个会话期 | 会话结束自动清理 |
| 外部API调用结果 | 1-30分钟 | 时间过期 |
缓存自动清理机制建议:
#!/bin/bash
# 缓存自动清理脚本
# 建议作为定期任务(cron)运行
CACHE_DIR="${HOME}/.claude/hooks/cache"
MAX_AGE=$((24 * 60 * 60)) # 24小时
echo "开始清理过期缓存..."
cleaned=0
find "$CACHE_DIR" -type f -name "*" | while read cache_file; do
file_age=$(($(date +%s) - $(stat -c %Y "$cache_file")))
if [ "$file_age" -gt "$MAX_AGE" ]; then
rm -f "$cache_file"
echo "已清理:${cache_file}(超过24小时)"
cleaned=$((cleaned + 1))
fi
done
# 清理空目录
find "$CACHE_DIR" -type d -empty -delete 2>/dev/null || true
echo "清理完成,共清理 ${cleaned} 个缓存文件"
提示: 缓存性能和新鲜度之间需要权衡。对于关键检测(如安全相关检查),建议使用较短的有效期或实时检测。对于非关键检测(如项目类型识别),可以使用较长的有效期来最大化性能收益。
四、懒加载和条件执行
懒加载(Lazy Loading)的核心思想是"只在真正需要的时候才执行"。并非所有Hook在所有情况下都需要完全执行,通过条件判断选择性执行逻辑,可以大幅减少无效的Hook执行。
4.1 只有在实际需要时才执行Hook逻辑
许多Hook都包含"是否真的需要执行"的判断条件。通过在Hook入口处设置快速返回的守卫条件,可以避免执行不必要的逻辑:
#!/bin/bash
# 懒加载模式:只在需要时执行完整逻辑
# 快速守卫:如果不需要执行,立即返回
quick_guard() {
# 检测是否在Claude Code环境中运行
if [ -z "${CLAUDE_CODE}" ]; then
echo "不在Claude Code环境中,跳过Hook"
exit 0
fi
# 检测是否在White列出的目录中
local allowed_dirs=("/home/user/projects" "/workspace")
local in_allowed=1
for dir in "${allowed_dirs[@]}"; do
if [[ "$PWD" == "${dir}"* ]]; then
in_allowed=0
break
fi
done
if [ "$in_allowed" -eq 1 ]; then
echo "当前目录不在允许列表中,跳过Hook"
exit 0
fi
}
# 延迟加载的功能模块(只在需要时才加载)
lazy_load_module() {
local module="$1"
local module_file="${HOME}/.claude/hooks/modules/${module}.sh"
if [ -f "$module_file" ]; then
# shellcheck source=/dev/null
source "$module_file"
echo "模块 ${module} 已加载"
fi
}
# 主流程
main() {
quick_guard
# 按需加载功能模块
if [ -f "package.json" ]; then
lazy_load_module "node_check"
run_node_check
fi
if [ -f "Dockerfile" ]; then
lazy_load_module "docker_check"
run_docker_check
fi
}
main "$@"
4.2 根据文件变更类型选择性执行
在处理 file-change Hook 时,可以根据发生变更的文件类型来决定执行哪些检查。不是所有文件变更都需要触发完整的Hook逻辑:
#!/bin/bash
# 根据文件变更类型选择性执行
# 适用于 file-change Hook
CHANGED_FILES="$@"
# 定义文件类型对应的检查模块
declare -A FILE_CHECKS
FILE_CHECKS["*.js"]="run_lint_check"
FILE_CHECKS["*.ts"]="run_type_check"
FILE_CHECKS["*.py"]="run_python_check"
FILE_CHECKS["Dockerfile"]="run_docker_check"
FILE_CHECKS["*.md"]="" # Markdown文件不触发任何检查
# 遍历变更文件
for file in $CHANGED_FILES; do
matched=0
for pattern in "${!FILE_CHECKS[@]}"; do
if [[ "$file" == $pattern ]]; then
check_func="${FILE_CHECKS[$pattern]}"
if [ -n "$check_func" ]; then
echo "文件 ${file} 变更,执行 ${check_func}"
$check_func "$file"
else
echo "文件 ${file} 变更为Markdown类型,跳过检查"
fi
matched=1
break
fi
done
if [ "$matched" -eq 0 ]; then
echo "文件 ${file} 类型未匹配任何检查规则,跳过"
fi
done
4.3 按分支/环境条件跳过不必要的检查
在某些分支或环境下,部分检查可能完全不需要执行。例如,在 feature 分支上可以跳过生产环境特有的检查:
#!/bin/bash
# 按分支和环境跳过检查
# 获取当前Git分支
get_current_branch() {
git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown"
}
# 获取环境类型
get_environment() {
if [ -n "${CI}" ]; then
echo "ci"
elif [ -n "${DEPLOY_ENV}" ]; then
echo "$DEPLOY_ENV"
else
echo "local"
fi
}
BRANCH=$(get_current_branch)
ENV=$(get_environment)
echo "当前分支:${BRANCH},环境:${ENV}"
# 不同分支执行不同检查
case "$BRANCH" in
main|master)
echo "主分支:执行完整检查"
run_full_checks
;;
develop)
echo "开发分支:执行标准检查"
run_standard_checks
;;
feature/*)
echo "特性分支:执行轻量检查"
run_light_checks
;;
*)
echo "其他分支:跳过部分检查"
run_minimal_checks
;;
esac
4.4 减少非必需的Hook执行
通过快速判断前置条件来减少非必需的Hook执行。以下是一些常见的快速跳过策略:
#!/bin/bash
# 快速跳过策略集合
# 策略1:基于时间窗口跳过(例如:距离上次执行不足30秒则跳过)
THROTTLE_FILE="${HOME}/.claude/hooks/.last_run"
THROTTLE_SECONDS=30
if [ -f "$THROTTLE_FILE" ]; then
last_run=$(cat "$THROTTLE_FILE")
now=$(date +%s)
elapsed=$((now - last_run))
if [ "$elapsed" -lt "$THROTTLE_SECONDS" ]; then
echo "[跳过] 距离上次执行仅${elapsed}秒,未达到${THROTTLE_SECONDS}秒间隔"
exit 0
fi
fi
echo "$(date +%s)" > "$THROTTLE_FILE"
# 策略2:基于文件大小快速跳过(超大文件不扫描)
check_file_size() {
local file="$1"
local max_size="${2:-10485760}" # 默认10MB
if [ -f "$file" ]; then
local size
size=$(stat -c%s "$file")
if [ "$size" -gt "$max_size" ]; then
echo "[跳过] 文件过大(${size}字节),跳过处理"
return 1
fi
fi
return 0
}
# 策略3:基于前置命令的退出码快速决定
if ! command -v node &> /dev/null; then
echo "[跳过] Node.js未安装,跳过所有Node相关检查"
exit 0
fi
# 策略4:维护已处理文件列表,避免重复处理
PROCESSED_FILE="${HOME}/.claude/hooks/.processed"
touch "$PROCESSED_FILE"
is_already_processed() {
local file="$1"
local file_hash
file_hash=$(md5sum "$file" | cut -d' ' -f1)
grep -q "^${file_hash}" "$PROCESSED_FILE" 2>/dev/null
}
mark_processed() {
local file="$1"
local file_hash
file_hash=$(md5sum "$file" | cut -d' ' -f1)
echo "$(date +%s) ${file_hash} ${file}" >> "$PROCESSED_FILE"
}
懒加载核心原则: ① 快速失败——在入口处用最轻量的检查决定是否需要继续;② 延迟加载——只在需要时才加载功能模块;③ 分级执行——根据不同场景(分支、环境、文件类型)执行不同深度的检查。这三条原则结合使用,可以将Hook的平均执行时间降低60-80%。
五、异步处理优化
对于无法通过缓存或懒加载完全消除的耗时操作,异步处理是有效的解决方案。异步的核心思想是"不阻塞主流程,后台完成任务"。不同类型的Hook适合不同的异步策略。
5.1 after Hook采用异步方式不阻塞主流程
after Hook在Claude Code生成回复后执行,虽然不直接影响用户等待时间,但如果执行过慢,仍然会占用系统资源,影响后续操作的响应速度。使用异步方式可以确保Hook执行不会阻塞主流程:
#!/bin/bash
# 异步执行after Hook
# 使用后台进程 + 等待机制
ASYNC_HOOK_SCRIPT="${HOME}/.claude/hooks/async_tasks.sh"
# 主Hook脚本:快速返回,异步执行耗时任务
main_hook() {
echo "after Hook已触发,主流程快速返回"
# 将耗时任务放到后台执行
nohup bash "$ASYNC_HOOK_SCRIPT" \
--task "$TASK_TYPE" \
--data "$HOOK_DATA" \
> /tmp/hook_async_$$.log 2>&1 &
ASYNC_PID=$!
echo "异步任务已启动(PID: ${ASYNC_PID}),日志:/tmp/hook_async_$$.log"
# 不等待后台进程,立即返回
# 前台只需要确认任务已启动即可
}
main_hook
异步任务脚本示例:
#!/bin/bash
# 异步任务脚本 - async_tasks.sh
# 在后台执行耗时操作
TASK_TYPE=""
DATA=""
# 解析参数
while [[ $# -gt 0 ]]; do
case "$1" in
--task) TASK_TYPE="$2"; shift 2 ;;
--data) DATA="$2"; shift 2 ;;
*) echo "未知参数:$1"; exit 1 ;;
esac
done
echo "[$(date '+%H:%M:%S')] 开始异步任务: ${TASK_TYPE}"
# 模拟耗时操作
case "$TASK_TYPE" in
"log_analysis")
echo "正在分析会话日志..."
sleep 2
echo "日志分析完成"
;;
"data_sync")
echo "正在同步数据..."
sleep 3
echo "数据同步完成"
;;
"report_generation")
echo "正在生成报告..."
sleep 5
echo "报告生成完成,保存到 /tmp/report_$$.md"
;;
esac
echo "[$(date '+%H:%M:%S')] 异步任务完成"
注意: 使用 nohup 启动的后台进程需要特别注意资源管理。如果不加以限制,大量后台进程可能耗尽系统资源。建议设置最大并发数和使用进程池控制。
5.2 将耗时操作放入后台执行
对于需要长时间运行的数据处理任务,可以采用以下模式:前台快速完成通知用户,后台继续处理数据记录:
#!/bin/bash
# 前台/后台分离模式
# 前台:快速完成通知用户
# 后台:继续处理数据记录
# ---------- 前台:快速响应 ----------
echo "处理已开始,请稍候..."
# 快速获取需要的信息
PROJECT_NAME=$(basename "$PWD")
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# 立即返回处理中的状态
echo "项目:${PROJECT_NAME}"
echo "时间:${TIMESTAMP}"
echo "状态:处理中..."
# ---------- 后台:耗时操作 ----------
{
# 重定向到日志文件
exec >> /tmp/hook_background_$$.log 2>&1
echo "[后台] 开始耗时操作"
# 模拟阶段1:数据收集
echo "[后台] 阶段1/3:收集数据..."
sleep 2
# 模拟阶段2:数据分析
echo "[后台] 阶段2/3:分析数据..."
sleep 3
# 模拟阶段3:生成报告
echo "[后台] 阶段3/3:生成报告..."
sleep 2
echo "[后台] 所有操作完成"
} &
BACKGROUND_PID=$!
echo "后台任务PID: ${BACKGROUND_PID}"
echo "您可以在 /tmp/hook_background_$$.log 查看进度"
5.3 使用作业控制管理异步任务
当系统中有多个异步任务时,需要合适的作业控制机制来管理并发、限制资源使用、处理任务状态:
#!/bin/bash
# 异步任务管理器
# 控制并发数、管理任务队列、处理任务状态
MAX_CONCURRENT=3
ACTIVE_JOBS=0
JOB_IDS=()
# 任务队列
declare -A TASK_QUEUE
TASK_QUEUE["task_1"]="长时间的代码分析任务"
TASK_QUEUE["task_2"]="生成性能统计报告"
TASK_QUEUE["task_3"]="同步远程仓库数据"
TASK_QUEUE["task_4"]="构建文档索引"
TASK_QUEUE["task_5"]="检查依赖更新"
# 启动任务
start_task() {
local task_id="$1"
local task_desc="${TASK_QUEUE[$task_id]}"
if [ "$ACTIVE_JOBS" -ge "$MAX_CONCURRENT" ]; then
echo "[队列] 任务 '${task_desc}' 等待中(已达最大并发数)"
return 1
fi
(
echo "[执行] 开始任务: ${task_desc}"
sleep $((RANDOM % 5 + 2)) # 模拟耗时
echo "[完成] 任务结束: ${task_desc}"
) &
local pid=$!
JOB_IDS+=("$pid")
ACTIVE_JOBS=$((ACTIVE_JOBS + 1))
echo "[启动] 任务 '${task_desc}' 已启动 (PID: ${pid})"
# 后台监控进程完成
(
wait "$pid" 2>/dev/null
# 进程结束后减少计数
) &
}
# 等待所有任务完成
wait_all() {
local timeout="${1:-60}"
local waited=0
echo "等待所有异步任务完成(超时:${timeout}秒)..."
while [ "$ACTIVE_JOBS" -gt 0 ] && [ "$waited" -lt "$timeout" ]; do
sleep 1
waited=$((waited + 1))
# 更新活跃作业数
local still_active=0
for pid in "${JOB_IDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
still_active=$((still_active + 1))
fi
done
ACTIVE_JOBS=$still_active
echo "活跃任务数:${ACTIVE_JOBS}(已等待${waited}秒)"
done
if [ "$ACTIVE_JOBS" -gt 0 ]; then
echo "[超时] 仍有 ${ACTIVE_JOBS} 个任务未完成"
else
echo "[完成] 所有异步任务已结束"
fi
}
# 启动所有任务
for task_id in "${!TASK_QUEUE[@]}"; do
start_task "$task_id"
done
# 等待完成(可选择是否阻塞)
# wait_all 60
5.4 异步任务的错误处理和重试机制
异步任务在后台执行时,错误处理尤为重要——没有用户即时反馈,错误可能被忽略。完善的错误处理和重试机制是异步Hook可靠运行的关键:
#!/bin/bash
# 异步任务错误处理和重试机制
RETRY_DIR="${HOME}/.claude/hooks/retry"
mkdir -p "$RETRY_DIR"
# 执行异步任务并记录错误
run_async_with_retry() {
local task_name="$1"
local max_retries="${2:-3}"
local retry_count=0
while [ "$retry_count" -lt "$max_retries" ]; do
# 执行任务
if "$@" 2>/tmp/async_err_$$.log; then
echo "[${task_name}] 执行成功"
return 0
fi
retry_count=$((retry_count + 1))
local exit_code=$?
echo "[${task_name}] 执行失败(第${retry_count}次),退出码:${exit_code}"
if [ "$retry_count" -lt "$max_retries" ]; then
local wait_time=$((retry_count * 2))
echo "[${task_name}] ${wait_time}秒后重试..."
sleep "$wait_time"
fi
done
# 重试耗尽,记录失败信息
local fail_record="${RETRY_DIR}/failed_$(date +%s)_${task_name}.log"
{
echo "任务名称:${task_name}"
echo "失败时间:$(date '+%Y-%m-%d %H:%M:%S')"
echo "重试次数:${max_retries}"
echo "错误信息:"
cat /tmp/async_err_$$.log
} > "$fail_record"
echo "[${task_name}] 重试${max_retries}次后仍然失败,记录已保存到 ${fail_record}"
return 1
}
# 使用示例
run_async_with_retry "data_sync" 3 ./sync_data.sh --source remote --target local &
异步处理核心要点总结:
1. after Hook适合采用异步模式,将耗时操作放到后台执行,主流程快速返回
2. 前台/后台分离模式:前台通知用户任务已开始,后台继续处理数据
3. 使用作业控制(nohup、wait、进程组)管理多个异步任务
4. 限制最大并发数(建议3-5个),避免系统资源耗尽
5. 异步任务必须要有完善的错误处理和重试机制
6. 记录异步任务日志,方便排查问题
六、综合优化策略与最佳实践
缓存、懒加载、异步三种优化手段并不是孤立的,它们可以组合使用,形成综合优化策略。以下是一个完整的优化实践框架:
#!/bin/bash
# 综合优化:缓存 + 懒加载 + 异步
# 适用于before Hook的完整优化示例
# ========== 配置 ==========
CACHE_DIR="${HOME}/.claude/hooks/cache"
LOG_DIR="${HOME}/.claude/hooks/logs"
mkdir -p "$CACHE_DIR" "$LOG_DIR"
HOOK_START=$(date +%s%N)
# ========== 快速守卫(懒加载策略) ==========
# 不满足条件时立即返回,零开销
if [ ! -f "package.json" ] && [ ! -f "Cargo.toml" ]; then
echo "未检测到已知项目类型,跳过Hook"
exit 0
fi
# ========== 缓存检查 ==========
CACHE_KEY="project_info_$(md5sum package.json 2>/dev/null | cut -d' ' -f1)"
CACHE_FILE="${CACHE_DIR}/$(echo ${CACHE_KEY} | md5sum | cut -d' ' -f1)"
if [ -f "$CACHE_FILE" ]; then
source "$CACHE_FILE"
echo "使用缓存的项目信息"
# 记录缓存命中
echo "$(date '+%Y-%m-%d %H:%M:%S') | CACHE_HIT | ${CACHE_KEY}" >> "${LOG_DIR}/cache_stats.log"
else
# ========== 实际检测(缓存未命中) ==========
echo "缓存未命中,执行完整检测..."
# 检测项目信息
PROJECT_NAME=$(node -p "require('./package.json').name" 2>/dev/null || echo "unknown")
PROJECT_VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")
# 保存到缓存
cat > "$CACHE_FILE" << EOF
PROJECT_NAME="${PROJECT_NAME}"
PROJECT_VERSION="${PROJECT_VERSION}"
EOF
echo "$(date '+%Y-%m-%d %H:%M:%S') | CACHE_MISS | ${CACHE_KEY}" >> "${LOG_DIR}/cache_stats.log"
fi
# ========== 耗时统计 ==========
HOOK_END=$(date +%s%N)
HOOK_MS=$(( (HOOK_END - HOOK_START) / 1000000 ))
echo "Hook总耗时:${HOOK_MS}ms"
# ========== 慢Hook告警(异步记录) ==========
if [ "$HOOK_MS" -gt 200 ]; then
# 异步记录慢Hook日志,不阻塞主流程
{
echo "慢Hook告警"
echo "时间:$(date '+%Y-%m-%d %H:%M:%S')"
echo "耗时:${HOOK_MS}ms"
echo "目录:${PWD}"
echo "---"
} >> "${LOG_DIR}/slow_hooks.log" &
fi
echo "before Hook处理完成"
性能影响评估
在添加新Hook前评估其对性能的影响,优先采用缓存友好型实现
渐进式优化
从最耗时的Hook开始优化,逐步降低整体执行时间
持续监控
建立性能基线和持续监控机制,防止性能退化
分层策略
结合缓存、懒加载、异步三种策略,针对不同场景选择最优方案
七、进一步思考
1. 如何对Hook系统进行自动化性能回归测试?
在CI/CD中集成Hook性能基准测试,每次Hook变更后自动运行,
对比性能基准数据。当新变更导致响应时间超过阈值时自动阻断。
2. 对于分布式场景下的Hook系统,如何优化跨网络调用的性能?
使用本地缓存减少网络请求频率,批量合并多个Hook调用,
对于非关键数据采用最终一致性策略。
3. 如何在多用户共享环境中管理Hook缓存?
使用用户隔离的缓存命名空间,设置统一的缓存清理策略,
对于共享的只读数据(如系统配置)使用全局缓存。
4. 当Hook数量增长到数百个时,如何保持性能稳定?
采用分层Hook执行引擎,分类管理不同类型的Hook,
建立Hook性能档案,自动降级超时的Hook,
使用事件驱动架构替代轮询模式。