Skip to content

Commit

Permalink
implement /_next/image for local requests (#48622)
Browse files Browse the repository at this point in the history
### What?

Adds image optimization to the next/image api for local requests

### Why?

More similarity to production. Smaller image downloads from the browser

### How?

see also vercel/turborepo#4649

fixes WEB-935

### Turbopack updates

* vercel/turborepo#4650 <!-- Tobias Koppers - reduce
size of TypeType from 32bytes to 16bytes -->
* vercel/turborepo#4648 <!-- Tobias Koppers - handle
chunk register in the sync runtime.none correctly -->
* vercel/turborepo#4609 <!-- OJ Kwon -
fix(ecmascript): eval assignop to the ident -->
* vercel/turborepo#4652 <!-- Tobias Koppers - allow
to pass constant values to free var references -->
* vercel/turborepo#4649 <!-- Tobias Koppers - Image
processing improvements -->
  • Loading branch information
sokra authored Apr 20, 2023
1 parent ec385de commit a7b0ae3
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 104 deletions.
1 change: 0 additions & 1 deletion packages/next-swc/crates/next-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ mod babel;
mod embed_js;
pub mod env;
mod fallback;
mod image;
pub mod manifest;
mod next_build;
pub mod next_client;
Expand Down
182 changes: 182 additions & 0 deletions packages/next-swc/crates/next-core/src/next_image/content_source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use std::collections::BTreeSet;

use anyhow::Result;
use turbo_binding::turbopack::{
core::{
asset::AssetContent,
ident::AssetIdentVc,
introspect::{Introspectable, IntrospectableVc},
server_fs::ServerFileSystemVc,
version::VersionedContent,
},
dev_server::source::{
query::QueryValue,
wrapping_source::{
encode_pathname_to_url, ContentSourceProcessor, ContentSourceProcessorVc,
WrappedContentSourceVc,
},
ContentSource, ContentSourceContent, ContentSourceContentVc, ContentSourceData,
ContentSourceDataFilter, ContentSourceDataVary, ContentSourceResultVc, ContentSourceVc,
NeededData, ProxyResult, RewriteBuilder,
},
image::process::optimize,
};
use turbo_tasks::{primitives::StringVc, Value};
use turbo_tasks_fs::FileSystem;

/// Serves, resizes, optimizes, and re-encodes images to be used with
/// next/image.
#[turbo_tasks::value(shared)]
pub struct NextImageContentSource {
asset_source: ContentSourceVc,
}

#[turbo_tasks::value_impl]
impl NextImageContentSourceVc {
#[turbo_tasks::function]
pub fn new(asset_source: ContentSourceVc) -> NextImageContentSourceVc {
NextImageContentSource { asset_source }.cell()
}
}

#[turbo_tasks::value_impl]
impl ContentSource for NextImageContentSource {
#[turbo_tasks::function]
async fn get(
self_vc: NextImageContentSourceVc,
path: &str,
data: Value<ContentSourceData>,
) -> Result<ContentSourceResultVc> {
let this = self_vc.await?;

let Some(query) = &data.query else {
let queries = ["url".to_string(), "w".to_string(), "q".to_string()]
.into_iter()
.collect::<BTreeSet<_>>();

return Ok(ContentSourceResultVc::need_data(Value::new(NeededData {
source: self_vc.into(),
path: path.to_string(),
vary: ContentSourceDataVary {
url: true,
query: Some(ContentSourceDataFilter::Subset(queries)),
..Default::default()
},
})));
};

let Some(QueryValue::String(url)) = query.get("url") else {
return Ok(ContentSourceResultVc::not_found());
};

let q = match query.get("q") {
None => 75,
Some(QueryValue::String(s)) => {
let Ok(q) = s.parse::<u8>() else {
return Ok(ContentSourceResultVc::not_found());
};
q
}
_ => return Ok(ContentSourceResultVc::not_found()),
};

let w = match query.get("w") {
Some(QueryValue::String(s)) => {
let Ok(w) = s.parse::<u32>() else {
return Ok(ContentSourceResultVc::not_found());
};
w
}
_ => return Ok(ContentSourceResultVc::not_found()),
};

// TODO: re-encode into next-gen formats.
if let Some(path) = url.strip_prefix('/') {
let wrapped = WrappedContentSourceVc::new(
this.asset_source,
NextImageContentSourceProcessorVc::new(path.to_string(), w, q).into(),
);
return Ok(ContentSourceResultVc::exact(
ContentSourceContent::Rewrite(
RewriteBuilder::new(encode_pathname_to_url(path))
.content_source(wrapped.as_content_source())
.build(),
)
.cell()
.into(),
));
}

// TODO: This should be downloaded by the server, and resized, etc.
Ok(ContentSourceResultVc::exact(
ContentSourceContent::HttpProxy(
ProxyResult {
status: 302,
headers: vec![("Location".to_string(), url.clone())],
body: "".into(),
}
.cell(),
)
.cell()
.into(),
))
}
}

#[turbo_tasks::value_impl]
impl Introspectable for NextImageContentSource {
#[turbo_tasks::function]
fn ty(&self) -> StringVc {
StringVc::cell("next image content source".to_string())
}

#[turbo_tasks::function]
fn details(&self) -> StringVc {
StringVc::cell("supports dynamic serving of any statically imported image".to_string())
}
}

#[turbo_tasks::value]
struct NextImageContentSourceProcessor {
path: String,
width: u32,
quality: u8,
}

#[turbo_tasks::value_impl]
impl NextImageContentSourceProcessorVc {
#[turbo_tasks::function]
pub fn new(path: String, width: u32, quality: u8) -> NextImageContentSourceProcessorVc {
NextImageContentSourceProcessor {
path,
width,
quality,
}
.cell()
}
}

#[turbo_tasks::value_impl]
impl ContentSourceProcessor for NextImageContentSourceProcessor {
#[turbo_tasks::function]
async fn process(&self, content: ContentSourceContentVc) -> Result<ContentSourceContentVc> {
let ContentSourceContent::Static(static_content) = *content.await? else {
return Ok(content);
};
let static_content = static_content.await?;
let asset_content = static_content.content.content().await?;
let AssetContent::File(file_content) = *asset_content else {
return Ok(content);
};
let optimized_file_content = optimize(
AssetIdentVc::from_path(ServerFileSystemVc::new().root().join(&self.path)),
file_content,
self.width,
u32::MAX,
self.quality,
);
Ok(ContentSourceContentVc::static_content(
AssetContent::File(optimized_file_content).into(),
))
}
}
104 changes: 5 additions & 99 deletions packages/next-swc/crates/next-core/src/next_image/mod.rs
Original file line number Diff line number Diff line change
@@ -1,100 +1,6 @@
use std::collections::BTreeSet;
pub(crate) mod content_source;
pub(crate) mod module;
pub(crate) mod source_asset;

use anyhow::Result;
use turbo_binding::turbopack::{
core::introspect::{Introspectable, IntrospectableVc},
dev_server::source::{
query::QueryValue, ContentSource, ContentSourceContent, ContentSourceData,
ContentSourceDataFilter, ContentSourceDataVary, ContentSourceResultVc, ContentSourceVc,
NeededData, ProxyResult,
},
};
use turbo_tasks::{primitives::StringVc, Value};

/// Serves, resizes, optimizes, and re-encodes images to be used with
/// next/image.
#[turbo_tasks::value(shared)]
pub struct NextImageContentSource {
asset_source: ContentSourceVc,
}

#[turbo_tasks::value_impl]
impl NextImageContentSourceVc {
#[turbo_tasks::function]
pub fn new(asset_source: ContentSourceVc) -> NextImageContentSourceVc {
NextImageContentSource { asset_source }.cell()
}
}

#[turbo_tasks::value_impl]
impl ContentSource for NextImageContentSource {
#[turbo_tasks::function]
async fn get(
self_vc: NextImageContentSourceVc,
path: &str,
data: Value<ContentSourceData>,
) -> Result<ContentSourceResultVc> {
let this = self_vc.await?;

let query = match &data.query {
None => {
let queries = [
"url".to_string(),
// TODO: support q and w queries.
]
.iter()
.cloned()
.collect::<BTreeSet<_>>();

return Ok(ContentSourceResultVc::need_data(Value::new(NeededData {
source: self_vc.into(),
path: path.to_string(),
vary: ContentSourceDataVary {
url: true,
query: Some(ContentSourceDataFilter::Subset(queries)),
..Default::default()
},
})));
}
Some(query) => query,
};

let url = match query.get("url") {
Some(QueryValue::String(s)) => s,
_ => return Ok(ContentSourceResultVc::not_found()),
};

// TODO: consume the assets, resize and reduce quality, re-encode into next-gen
// formats.
if let Some(path) = url.strip_prefix('/') {
return Ok(this.asset_source.get(path, Default::default()));
}

// TODO: This should be downloaded by the server, and resized, etc.
Ok(ContentSourceResultVc::exact(
ContentSourceContent::HttpProxy(
ProxyResult {
status: 302,
headers: vec![("Location".to_string(), url.clone())],
body: "".into(),
}
.cell(),
)
.cell()
.into(),
))
}
}

#[turbo_tasks::value_impl]
impl Introspectable for NextImageContentSource {
#[turbo_tasks::function]
fn ty(&self) -> StringVc {
StringVc::cell("next image content source".to_string())
}

#[turbo_tasks::function]
fn details(&self) -> StringVc {
StringVc::cell("suports dynamic serving of any statically imported image".to_string())
}
}
pub use content_source::NextImageContentSourceVc;
pub use module::StructuredImageModuleTypeVc;
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ use turbo_binding::{
},
};

use self::source::StructuredImageSourceAsset;

pub(crate) mod source;
use super::source_asset::StructuredImageSourceAsset;

/// Module type that analyzes images and offers some meta information like
/// width, height and blur placeholder as export from the module.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use turbo_binding::{
};
use turbo_tasks::trace::TraceRawVcs;

use crate::image::StructuredImageModuleTypeVc;
use crate::next_image::StructuredImageModuleTypeVc;

/// Returns a rule which applies the Next.js page export stripping transform.
pub async fn get_next_pages_transforms_rule(
Expand Down

0 comments on commit a7b0ae3

Please sign in to comment.