Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lazy loading to images #457

Closed

Conversation

bergice
Copy link
Contributor

@bergice bergice commented Jun 25, 2021

Resolves #450

Copy link
Member

@emteknetnz emteknetnz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach looks good

*
* @var bool
*/
protected $lazyLoad = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
protected $lazyLoad = true;
private $lazyLoad = true;

*
* @return bool
*/
public function IsLazyLoaded()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function IsLazyLoaded()
public function getIsLazyLoaded(): bool

*/
public function IsLazyLoaded()
{
if (!Config::inst()->get('SilverStripe\\Assets\\Image', 'lazy_loading_enabled')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!Config::inst()->get('SilverStripe\\Assets\\Image', 'lazy_loading_enabled')) {
if (!Image::getLazyLoadingEnabled()) {

* @param bool $lazyLoad
* @return $this
*/
public function LazyLoad($lazyLoad)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function LazyLoad($lazyLoad)
public function setLazyLoad(bool $lazyLoad)

you'll also need a corredsponding function getLazyLoad(): bool

*/
public function LazyLoad($lazyLoad)
{
$this->lazyLoad = $lazyLoad;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check that width and height exist on the image

If not, return false

Will need to unit test this

@@ -1 +1 @@
<img src="$URL.ATT" alt="$Title.ATT" />
<img src="$URL.ATT" alt="$Title.ATT"<% if $IsLazyLoaded %> loading="lazy" width="$Width.ATT" height="$Height.ATT"<% end_if %> />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add width + height regardless whether $IsLazyLoaded is true or not

@bergice bergice force-pushed the pulls/1/lazy-load-images branch from 14ee61e to df9373c Compare June 28, 2021 23:35
@bergice
Copy link
Contributor Author

bergice commented Jun 29, 2021

@emteknetnz I've addressed your feedback.
No idea what's going on with failing build.

Copy link
Contributor

@maxime-rainville maxime-rainville left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ImageManipulation is applied to File and DBFile as well as Image. I would expect the implementation and tests accounts for this (e.g. What's the expected results if you call getIsLazyLoaded on a PDF File? Where's the test for this scenario.)

* @param bool $lazyLoad
* @return $this
*/
public function setLazyLoad($lazyLoad)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this works.

Suggested change
public function setLazyLoad($lazyLoad)
public function setLazyLoad($lazyLoad): self

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool $lazyLoad

@@ -1 +1 @@
<img src="$URL.ATT" alt="$Title.ATT" />
<img src="$URL.ATT" alt="$Title.ATT"<% if $IsLazyLoaded %> loading="lazy"<% end_if %> width="$Width.ATT" height="$Height.ATT" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still think we should use getAttributes/getAttributeHTML here te be consistent with other parts of the CMS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but that is out of scope of this issue.

Copy link
Contributor

@maxime-rainville maxime-rainville Jul 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think taking the time to analyse a problem and finding the most sensible way of solving it is a question of "scope".

If you don't like the "getAttributes/getAttributeHTML" approach or if you think it's not worth the extra work, explain why ... just don't call it out-of-scope.

Copy link
Contributor Author

@bergice bergice Jul 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scope of this project is to make images lazy load, not create fancy new API's that will increase the total time we spend on this epic.

I like the idea of having an API for rendering attributes that is not exclusive to form fields, but it would require some refactoring and is considered a generic improvement / minor feature.
If you really need this done then either do it yourself or create another issue.

In addition to that, if any refactoring should be done regarding front-end rendering of images, it should be refactoring to using React.

TLDR; it's not worth the extra work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just need to copy this bit of logic and adjust it for your purposes https://github.com/silverstripe/silverstripe-framework/blob/dcdc25500be729f4340d42f8a4fc797461f862f3/src/Forms/FormField.php#L680-L757

It shouldn't be that long. Plus you'll need refactor the entire thing anyway to address the problem with sequentially rendered images.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just need to copy this bit of logic and adjust it for your purposes https://github.com/silverstripe/silverstripe-framework/blob/dcdc25500be729f4340d42f8a4fc797461f862f3/src/Forms/FormField.php#L680-L757

"Just".

My previous arguments against doing this still stand.
In addition to that I'm against copying identical code. If anything, we should be refactoring the other methods as well so they use the same code, which is not a trivial effort.

It shouldn't be that long. Plus you'll need refactor the entire thing anyway to address the problem with sequentially rendered images.

That is not related.

$this->assertFalse($image->getIsLazyLoaded(), 'Images can be eager loaded on a per-image basis');

$image->setLazyLoad(true);
Config::modify()->set(Image::class, 'lazy_loading_enabled', false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bergice bergice force-pushed the pulls/1/lazy-load-images branch 2 times, most recently from c98540b to afe2525 Compare June 30, 2021 04:57
* @param bool $lazyLoad
* @return self $this
*/
public function setLazyLoad(bool $lazyLoad): self
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have this in your template

<h2>Not Lazy Loaded</h2>
$ScaleWidth(100).setLazyLoad(0)
$setLazyLoad(0)
$setLazyLoad(1).$setLazyLoad(0)

<h2>Lazy Loaded</h2>
$ScaleWidth(100)
$Me
$setLazyLoad(1)

All the rendered images will not have a loading attribute because only the first rendered image is honoured. If you reverse the two blocks all the Image will have the loading attribute.

setLazyLoad needs to return a copy of itself like the other ImageManipulation function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method probably should be start with an upper case letter to match the other image manipulation methods.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to remove the explicit bool type so people can provide the value as 'false' or '0' because of a limitation in our SS rendering engine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method probably should be start with an upper case letter to match the other image manipulation methods.

Agree, I figured we should do that as well.

We need to remove the explicit bool type so people can provide the value as 'false' or '0' because of a limitation in our SS rendering engine.

That's not gonna work because it will just set the variable to false as a string.
We should really just expect a 0/1 value like every other template API already does.

Copy link
Contributor Author

@bergice bergice Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the rendered images will not have a loading attribute because only the first rendered image is honoured. If you reverse the two blocks all the Image will have the loading attribute.

setLazyLoad needs to return a copy of itself like the other ImageManipulation function.

I'm struggling to see how this would matter in a real world scenario. What would be the point of lazy loading and not lazy load the same image? After all, it's pointing to the same URL.

The only use case I see is if you manipulate the image, and if you do that it will create a copy and toggling the lazy load for the same image with a different variant will work as expected.

I'm reluctant to create a copy of the image, purely for the sake of doing something that has no real world value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a developer, I shouldn't have to worry about how SetLazyLoad(false) will be cast internally. It should just work. So in the method, we should just convert a 'false' string to an actual boolean.

I'm struggling to see how this would matter in a real world scenario. What would be the point of lazy loading and not lazy load the same image? After all, it's pointing to the same URL.

SSViewer tries to cache things as much as possible as it goes along. You could end up with weird scenario where you have an include that gets call in different context. We'll just end up with weird bugs and have inconsistent result if we do it this way.

That's why ImageManipulation has all this convoluted logic and jumps through all these hoops to create duplicate object of Image.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a developer, I shouldn't have to worry about how SetLazyLoad(false) will be cast internally. It should just work.

Yes, it should. However this is something that should be solved for templates in general, not just hardcoded into individual methods.

It is clear that this is something that will keep being an issue unless we fix it properly, and I don't think this "duct-taping" fix approach is the right way to solve it. silverstripe/silverstripe-cms@ed0680a

SSViewer tries to cache things as much as possible as it goes along. You could end up with weird scenario where you have an include that gets call in different context. We'll just end up with weird bugs and have inconsistent result if we do it this way.

That's why ImageManipulation has all this convoluted logic and jumps through all these hoops to create duplicate object of Image.

I'm not sure about that yet. Do you have an example? I'm under the impression it duplicates the image after altering it / when the variant changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a developer, I shouldn't have to worry about how SetLazyLoad(false) will be cast internally. It should just work.

Yes, it should. However this is something that should be solved for templates in general, not just hardcoded into individual methods.

It is clear that this is something that will keep being an issue unless we fix it properly, and I don't think this "duct-taping" fix approach is the right way to solve it.

FYI this has been resolved in 5.x/dev-master with silverstripe/silverstripe-framework#8152 (and silverstripe/silverstripe-framework#8456 for null values, which was a marginally more invasive change). I treated it as an API change at the time, but I suppose we could look at whether it’d be possible to cherry-pick those changes into 4.x?

I wouldn’t be opposed to “duct-taping” something like this for now. It’s one line of zero-maintenance code to reduce a bunch of confusion/extra documentation for developers using this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was suggest cherry-picking as well. Might be fine as long as we consider it a bug.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It duplicates the PHP object, but it doesn't dupilcate the actual File database entry or the physical file.

The idea is to stack up all the image manipulation calls, but wait until you actually render the image at the end to execute them. Otherwise, you would be running a bunch of expensive (and potentially unneeded) image manipulation operations immediately when calrling a method like CropHeight.

My guess is you probably just need to do something like this to duplicate you current object.

$this->manipulate('', function($callback, $store, $filename, $hash, $variant) {
    return [
    'Filename' => $filename,
    'Hash' => $hash,
    'Variant' => $this->getVariant()
]
});

Then you set a lazy loading flag on you duplicate.

You need to make sure that your lazy loading flag gets copied across to any follow up calls to ImageManipulation::manipulate().

@@ -1 +1 @@
<img src="$URL.ATT" alt="$Title.ATT" />
<img src="$URL.ATT" alt="$Title.ATT"<% if $IsLazyLoaded %> loading="lazy"<% end_if %> width="$Width.ATT" height="$Height.ATT" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just need to copy this bit of logic and adjust it for your purposes https://github.com/silverstripe/silverstripe-framework/blob/dcdc25500be729f4340d42f8a4fc797461f862f3/src/Forms/FormField.php#L680-L757

It shouldn't be that long. Plus you'll need refactor the entire thing anyway to address the problem with sequentially rendered images.

@emteknetnz
Copy link
Member

Unfortunately this approach does not appear to do what it needs to work as intended. While the unit tests pass, adding an image in the template as $MyImage.LazyLoad(false) doesn't work because:

a) the argument is passed to PHP as a string "false" - which is fixiable within the getLazyLoaded function, however
b) the value in the private field $lazyLoad is not retained, I believe because ImageManipulation is a bit of an abstraction over DBFile

I'm closing this in favour of the alternative approach here #458

@emteknetnz emteknetnz closed this Jul 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SSViewer image renders loading attribute
4 participants