[IMP] web, *: Add lazy loading for images in kanban

This commit adds the loading="lazy" attribute to every img tags compiled
by the kanban compiler and modifies some kanban archs in order to use
img tags instead of divs with background-image style to load images
(this allows again the loading="lazy" attribute to work).
The purpose is to avoid loading images that are outside the user viewport
at all time in order to reduce the load on the network when new images are
loaded.
Steps to test: find all views that are using the o_kanban_image or o_kanban_image_fill_left
class and ensure all the images in these views appear in the network tab of devtools
when they enter the user viewport. Example: res.partners, hr.employee, ...

task-3102207

closes odoo/odoo#117049

Related: odoo/enterprise#39560
Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
Julien Carion (juca)
2023-04-14 15:05:56 +02:00
parent cecadbfb8f
commit 1f6c655cbc
10 changed files with 74 additions and 21 deletions
+3 -1
View File
@@ -273,7 +273,9 @@
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill">
<div class="o_kanban_image" t-attf-style="background-image:url('#{kanban_image('fleet.vehicle', 'image_128', record.id.raw_value)}')"/>
<div class="o_kanban_image">
<img t-attf-src="#{kanban_image('fleet.vehicle', 'image_128', record.id.raw_value)}" t-att-alt="record.id.value"/>
</div>
<div class="oe_kanban_details">
<strong class="o_kanban_record_title">
<t t-if="record.license_plate.raw_value"><field name="license_plate"/>:</t> <field name="model_id"/>
@@ -1,11 +1,14 @@
div.o_field_widget.o_field_background_image {
display: inline-block;
> div {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
> img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
position: absolute;
top: 0;
left: 0;
}
}
@@ -1,10 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr.BackgroundImage" owl="1">
<div
<img
loading="lazy"
t-att-data-tooltip-template="hasTooltip and tooltipAttributes.template"
t-att-data-tooltip-info="hasTooltip and tooltipAttributes.info"
t-att-data-tooltip-delay="hasTooltip and props.zoomDelay"
t-attf-style="background-image: url('#{getUrl(props.previewImage or props.name)}');"/>
t-attf-src="#{getUrl(props.previewImage or props.name)}"
alt="Binary file"
/>
</t>
</templates>
+6 -4
View File
@@ -163,8 +163,9 @@
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill">
<div class="o_kanban_image_fill_left d-none d-md-block"
t-attf-style="background-image:url('#{kanban_image('lunch.product', 'image_128', record.id.raw_value)}')"/>
<div class="o_kanban_image_fill_left d-none d-md-block">
<img t-attf-src="#{kanban_image('lunch.product', 'image_128', record.id.raw_value)}" t-att-alt="record.id.value"/>
</div>
<div class="oe_kanban_details">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
@@ -254,8 +255,9 @@
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_kanban_record">
<div class="o_kanban_image_fill_left d-none d-md-block"
t-attf-style="background-image:url('#{kanban_image('lunch.product.category', 'image_128', record.id.raw_value)}')"/>
<div class="o_kanban_image_fill_left d-none d-md-block">
<img t-attf-src="#{kanban_image('lunch.product.category', 'image_128', record.id.raw_value)}" t-att-alt="record.id.value"/>
</div>
<div class="oe_kanban_details">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
@@ -30,6 +30,7 @@
</t>
</div>
<img
loading="lazy"
class="img img-fluid w-100"
alt="Binary file"
t-att-src="this.getUrl(props.previewImage or props.name)"
@@ -26,7 +26,8 @@ export class KanbanCompiler extends ViewCompiler {
this.ctx.readonly = "read_only_mode";
this.compilers.push(
{ selector: ".oe_kanban_colorpicker", fn: this.compileColorPicker },
{ selector: "t[t-call]", fn: this.compileTCall }
{ selector: "t[t-call]", fn: this.compileTCall },
{ selector: "img", fn: this.compileImage }
);
}
@@ -95,7 +96,14 @@ export class KanbanCompiler extends ViewCompiler {
return compiled;
}
/**
* @returns {Element}
*/
compileImage(el) {
const element = el.cloneNode(true);
element.setAttribute("loading", "lazy");
return element;
}
/**
* @returns {Element}
*/
@@ -183,9 +183,21 @@
position: center;
repeat: no-repeat;
}
> img:not(.o_kanban_image_inner_pic) {
object-fit: cover;
object-position: center;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
&.o_kanban_image_full {
background-size: contain;
> img {
object-fit: contain;
}
}
}
@@ -194,6 +206,7 @@
// Eg. In partners list we use to fill user picture only, keeping the
// default design for company logos.
.o_kanban_image {
position: relative;
margin-right: var(--KanbanRecord-padding-h);
flex: 0 0 var(--KanbanRecord__image-width);
min-height: var(--KanbanRecord__image-width);
@@ -203,6 +216,15 @@
repeat: no-repeat;
position: center;
}
> img:not(.o_kanban_image_inner_pic) {
object-fit: cover;
object-position: center;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
@include media-breakpoint-down(md) {
flex-basis: var(--KanbanRecord__image--fill-width);
@@ -9538,6 +9538,11 @@ QUnit.module("Views", (hooks) => {
'img[data-src*="/web/image"][data-src*="&id=1"]'
);
assert.strictEqual(imageOnRecord.length, 1, "partner with image display image by url");
assert.strictEqual(
imageOnRecord[0].loading,
"lazy",
"img in kanban view should be lazy loaded"
);
});
QUnit.test("test displaying image (write_date field)", async (assert) => {
@@ -235,14 +235,14 @@
<t t-set="placeholder" t-value="'/website_slides/static/src/img/channel-training-default.jpg'"/>
<t t-if="record.image_128.raw_value" t-set="image" t-value="kanban_image('slide.slide', 'image_128', record.id.raw_value, placeholder)"/>
<t t-else="" t-set="image" t-value="'/website_slides/static/src/img/channel-training-default.jpg'"/>
<div class="o_kanban_image_fill_left d-none d-md-block"
t-attf-style="background-image:url('#{image}')">
<div class="o_kanban_image_fill_left d-none d-md-block">
<img t-attf-src="#{image}" t-att-alt="record.id.value"/>
<img class="o_kanban_image_inner_pic"
t-att-alt="record.channel_id.value"
t-att-src="kanban_image('slide.channel', 'image_128', record.channel_id.raw_value)"/>
</div>
<div class="o_kanban_image rounded-circle d-md-none"
t-attf-style="background-image:url('#{image}')">
<div class="o_kanban_image d-md-none">
<img class="rounded-circle" t-attf-src="#{image}" t-att-alt="record.id.value"/>
<img class="o_kanban_image_inner_pic"
t-att-alt="record.channel_id.value"
t-att-src="kanban_image('slide.channel', 'image_128', record.channel_id.raw_value)"/>
+11 -4
View File
@@ -481,16 +481,23 @@
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_res_partner_kanban">
<t t-if="!record.is_company.raw_value">
<t t-set="background_image" t-value="kanban_image('res.partner', 'avatar_128', record.id.raw_value)"/>
<div class="o_kanban_image_fill_left d-none d-md-block" t-attf-style="background-image:url('#{background_image}')">
<t t-set="avatar_image" t-value="kanban_image('res.partner', 'avatar_128', record.id.raw_value)"/>
<div class="o_kanban_image_fill_left d-none d-md-block">
<img t-attf-src="#{avatar_image}" t-att-alt="record.id.value"/>
<img class="o_kanban_image_inner_pic" t-if="record.parent_id.raw_value" t-att-alt="record.parent_id.value" t-att-src="kanban_image('res.partner', 'avatar_128', record.parent_id.raw_value)"/>
</div>
<div class="o_kanban_image d-md-none" t-attf-style="background-image: url(#{background_image})">
<div class="o_kanban_image d-md-none d-block">
<img t-attf-src="#{avatar_image}" t-att-alt="record.id.value"/>
<img class="o_kanban_image_inner_pic" t-if="record.parent_id.raw_value" t-att-alt="record.parent_id.value" t-att-src="kanban_image('res.partner', 'avatar_128', record.parent_id.raw_value)"/>
</div>
</t>
<t t-else="">
<div class="o_kanban_image_fill_left o_kanban_image_full" t-attf-style="background-image: url(#{kanban_image('res.partner', 'avatar_128', record.id.raw_value)})" role="img"/>
<div class="o_kanban_image_fill_left d-none d-md-block o_kanban_image_full">
<img t-attf-src="#{kanban_image('res.partner', 'avatar_128', record.id.raw_value)}" role="img" t-att-alt="record.id.value"/>
</div>
<div class="o_kanban_image d-md-none d-block o_kanban_image_full">
<img t-attf-src="#{kanban_image('res.partner', 'avatar_128', record.id.raw_value)}" role="img" t-att-alt="record.id.value"/>
</div>
</t>
<div class="ribbon ribbon-top-right" attrs="{'invisible': [('active', '=', True)]}">
<span class="bg-danger">Archived</span>