根本原因是Composer不区分开发与运行时依赖,且包的require-dev、宽松版本约束、可选依赖(suggests)、provide声明及autoload-dev配置共同导致冗余包被安装。

为什么 composer install 会装一堆没用的包?
根本原因在于 Composer 默认不区分「开发依赖」和「运行时依赖」的安装边界,且很多包的 require-dev 里嵌套了重型工具(比如 phpunit、infection、larastan),而这些又可能拉入 symfony/console、doctrine/annotations 等非生产必需组件。更麻烦的是,某些包在 composer.json 中声明了宽松版本约束(如 "^2.0 || ^3.0"),导致 Composer 解析时引入更多候选包参与依赖求解,间接扩大依赖图谱。
composer install --no-dev --optimize-autoloader 是基础但不够
这条命令能跳过 require-dev 并生成静态类映射,但它不解决「主依赖自身带冗余子依赖」的问题。比如你只用 monolog/monolog 的 Logger 和 StreamHandler,但它默认会装 aws/aws-sdk-php(用于 S3 handler)、elasticsearch/elasticsearch(用于 ES handler)等可选依赖——这些不会被自动剔除。
- 必须手动检查
composer show monolog/monolog输出里的suggests区块,确认哪些可选扩展实际未使用 - 在
composer.json的replace或conflict段声明排除,例如:"conflict": { "aws/aws-sdk-php": "*", "elasticsearch/elasticsearch": "*" } - 注意:部分包(如
guzzlehttp/guzzle)的可选依赖是通过provide声明的,conflict无效,得用replace+ 空包占位
用 composer why 和 composer depends 定位隐藏依赖源
你以为删了 phpunit 就干净了?不一定。可能是某个工具包(如 phpstan/phpstan)在 require-dev 里间接依赖它,或者你的框架封装包(如 laravel/framework)把测试辅助类打进 require 了。这时候靠猜没用,得查链路:
-
composer why aws/aws-sdk-php查谁主动拉入了这个包 -
composer depends --tree monolog/monolog看谁在依赖它、是否属于 dev-only 路径 - 重点盯
dev-master、dev-develop这类不稳定版本——它们常带未收敛的依赖树,换成稳定 tag 更可控
生产环境必须禁用 autoload-dev 并清理 vendor/autoload.php
即使你加了 --no-dev,只要 vendor/autoload.php 里还加载了 autoload-dev 配置(常见于某些包的 composer.json),PHP 自动加载器仍可能注册开发专用的类路径,导致内存占用上升、OPcache 冗余条目增多。
- 检查
vendor/composer/autoload_dev.php是否被任何地方require;如果有,说明某包在生产代码里调用了class_exists('PHPUnit_Framework_TestCase')这类判断逻辑 - 用
composer dump-autoload --optimize --no-dev重建自动加载映射,确保autoload-dev.php不被写入autoload_static.php - 上线前执行
find vendor -name "*test*" -o -name "*fixture*" -o -name "*stub*" | xargs rm -rf(谨慎!先ls确认)——有些包把测试文件打进了正式发布包
真正瘦下来的包体积,往往藏在你没意识到的 suggests、provide 和 autoload-dev 交叉影响里。别只盯着 composer.json 的 require 字段,那些没被显式声明、却因兼容性或历史原因悄悄进来的包,才是最耗资源的隐形负载。











