composer install --no-dev 不够用,因它仅移除 require-dev 包,却无法剔除生产依赖中未实际调用的代码(如 monolog 中仅用 logger 却打包全部 handler),且 composer 无 ast 分析能力,不能实现 tree-shaking。

为什么 composer install --no-dev 不够用?
Serverless 环境对包体积极其敏感,composer install --no-dev 只删了 require-dev 里的包,但大量生产依赖中真正被调用的代码可能只占 5%–15%。比如 monolog/monolog 被引入只是因为用了它的 Logger,但整个包含 20+ handler 类、formatter、processor,全打进 zip 就是纯浪费。
更麻烦的是:Composer 本身不分析运行时调用链,它按 autoload 规则把整个包目录扔进 vendor,根本不管你的函数里是否 new S3Client() 还是只调了 json_encode()。
所以光靠 Composer 命令无法实现 tree-shaking —— 它不是 JS bundler,没有 AST 分析能力。
能用的替代方案只有两个:手动裁剪 + 静态分析工具
目前 PHP 生态没有成熟、开箱即用的 Composer tree-shaking 工具。所谓“裁剪最小 vendor”,本质是两步:先识别实际用到的类/函数,再删掉未引用的文件。可行路径如下:
-
优先手动白名单:在部署前写个脚本,用
grep -r "use.*Monolog\" --include="*.php" src/找出真实引用的命名空间,再结合composer show --tree锁定最小必要子包(比如只要monolog/monolog,不要aws/aws-sdk-php的全部依赖) -
用
php-scoper+roave/better-reflection做粗粒度剪枝:它能扫描入口文件,提取所有new、static::、function_exists等调用,生成“可能用到”的类列表;但注意它不处理动态类名(new $class)、call_user_func、反射加载,这些会漏判 -
禁用 autoload 重写,改用显式 require:在 Lambda 入口里不用
vendor/autoload.php,而是只require_once 'vendor/monolog/monolog/src/Monolog/Logger.php'—— 这最彻底,但维护成本高,适合稳定、无扩展需求的函数
容易被忽略的坑:autoload 类型和 PSR-4 映射破坏裁剪结果
很多包(如 guzzlehttp/guzzle)同时声明了 psr-4 和 files autoload,后者会强制加载一堆全局函数(src/functions_include.php)。你删了文件但没删 files 条目,Lambda 启动时直接报 Warning: require(...): failed to open stream。
检查方法:运行 composer dump-autoload --no-dev -o 后,打开 vendor/composer/autoload_files.php,逐行核对是否存在已删除的路径。
安全做法是:裁剪后用 php -l 扫描所有 .php 文件,并在本地模拟 Lambda 运行环境(如 docker run --rm -v $(pwd):/var/task public.ecr.aws/lambda/php:8.2)跑一次冷启动,看是否抛出 Class not found 或 require(): failed。
AWS Lambda 层 + 分离 runtime 与业务 vendor 更可靠
别把所有依赖塞进函数 zip。把稳定、体积大的依赖(如 aws/aws-sdk-php、ext-zip 扩展绑定的库)打包成 Lambda Layer,函数层只放自己写的代码 + 极小的 glue 依赖(如 psr/log 接口包)。
这样做的好处:
- Layer 可复用,多个函数共享同一份 vendor,上传快、版本好管理
- 函数 zip 体积常能压到 1MB 以内,跳过 Lambda 的解压耗时(尤其冷启动)
- 规避了
vendor内部符号冲突(比如两个函数各自 vendor 里有不同版本的symfony/polyfill)
Layer 的 vendor 仍建议用 --no-dev + 手动删掉 tests/、docs/、examples/ 目录 —— 这些删起来零风险,且立竿见影。
真要上自动化裁剪,得接受它是个半手工活:没有一键 magic 命令,最省心的路径反而是控制依赖源头 —— 别让 composer.json 里出现“看起来有用但其实没调用”的包。










