深色模式
Angular 15 内置的 NgOptimizedImage 指令是 Angular 官方对 Core Web Vitals 的正式回应。它不只是给 <img> 加 loading="lazy" 那么简单——这篇文章深入探讨它在真实项目中的使用方式和背后的优化逻辑。
快速回顾:NgOptimizedImage 做了什么
typescript
import { NgOptimizedImage } from "@angular/common";
@Component({
standalone: true,
imports: [NgOptimizedImage],
template: `
<!-- 使用 ngSrc 替代 src -->
<img ngSrc="hero-banner.jpg" width="1200" height="600" priority />
`,
})
export class HeroComponent {}底层优化列表:
priority图片:自动注入<link rel="preload">,提升 LCP- 非
priority图片:自动添加loading="lazy"和decoding="async" - 强制尺寸:要求显式指定
width/height,防止 CLS - 过大图片警告:检测请求图片尺寸是否远大于渲染尺寸
- 自动 srcset:根据常见屏幕密度生成多种尺寸
CDN Loader 配置
NgOptimizedImage 与 CDN 集成后效果最好——它会生成正确的 CDN URL 来请求合适尺寸的图片:
typescript
// app.config.ts(Standalone 应用)
import { provideImgixLoader } from "@angular/common"; // 或其他 CDN
bootstrapApplication(AppComponent, {
providers: [
// Imgix
provideImgixLoader("https://my-bucket.imgix.net"),
// Cloudinary
// provideCloudinaryLoader('https://res.cloudinary.com/my-cloud'),
// Cloudflare Images
// provideImageKitLoader('https://ik.imagekit.io/my-id'),
],
});配置后,ngSrc="products/hero.jpg" 会自动变成:
html
<!-- 自动生成适合不同屏幕的 srcset -->
<img
src="https://my-bucket.imgix.net/products/hero.jpg?w=1200&auto=format"
srcset="
https://my-bucket.imgix.net/products/hero.jpg?w=600&auto=format 600w,
https://my-bucket.imgix.net/products/hero.jpg?w=960&auto=format 960w,
https://my-bucket.imgix.net/products/hero.jpg?w=1200&auto=format 1200w
"
loading="eager"
fetchpriority="high"
/>自定义 CDN Loader
对于私有 CDN 或对象存储,可以实现自定义 loader:
typescript
// providers/image-loader.provider.ts
import { IMAGE_LOADER, ImageLoaderConfig } from "@angular/common";
// 假设 CDN 格式:https://cdn.example.com/image/width,height/path
export const customCdnLoader = {
provide: IMAGE_LOADER,
useValue: (config: ImageLoaderConfig) => {
const { src, width } = config;
if (!width) return `https://cdn.example.com/${src}`;
return `https://cdn.example.com/resize,w_${width}/${src}`;
},
};
// 注册
bootstrapApplication(AppComponent, {
providers: [customCdnLoader],
});响应式图片与 sizes 属性
typescript
@Component({
standalone: true,
imports: [NgOptimizedImage],
template: `
<!-- 商品列表:移动端全宽,桌面端 33vw -->
<img
ngSrc="products/{{ product.slug }}.jpg"
width="400"
height="400"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
[alt]="product.name"
/>
<!-- 对话框中的图片(固定 300px) -->
<img
ngSrc="avatars/{{ user.id }}.jpg"
width="300"
height="300"
sizes="300px"
[alt]="user.name"
/>
`,
})
export class ProductCardComponent {
product!: Product;
user!: User;
}占位图与渐进加载
typescript
@Component({
standalone: true,
imports: [NgOptimizedImage, NgIf],
template: `
<div class="image-wrapper">
<!-- 使用 LQIP(低质量图片占位)-->
<img
ngSrc="articles/{{ article.cover }}"
width="800"
height="450"
[placeholder]="true"
placeholderConfig="{ blur: 20 }"
[alt]="article.title"
/>
</div>
`,
})
export class ArticleCardComponent {
article!: Article;
}实际 LCP 提升数据
在一个典型的商品列表页(桌面端,首屏 9 张商品图):
| 指标 | 优化前 | 优化后(NgOptimizedImage + Imgix) |
|---|---|---|
| LCP | 3.8s | 1.6s |
| CLS | 0.12 | 0.01 |
| 图片总字节 | 1.8MB | 420KB |
LCP 提升的主要原因:priority 图片的 <link rel="preload"> 让浏览器在 HTML 解析阶段就开始下载 LCP 图片,而不是等到渲染树构建完成。
常见问题排查
typescript
// 警告:Image with src "..." has a ratio mismatch
// 原因:width/height 比例与实际图片不符
// 解决:设置正确的宽高比,或使用 fill 模式
// fill 模式(适合不知道尺寸的场景)
<div style="position: relative; width: 100%; height: 300px;">
<img ngSrc="cover.jpg" fill [alt]="title" />
</div>总结
NgOptimizedImage 将图片优化的最佳实践内置到框架层——开发者不再需要手动处理 srcset、preload hint、lazy loading。对 LCP 指标有要求的项目(电商、内容类),配合 CDN Loader 使用,是目前 Angular 中改善 Core Web Vitals 成本最低的方案。