tjoskar / ng-lazyload-image

🖼 A small library for lazy loading images for Angular apps with zero dependencies
https://naughty-bose-ec1cfc.netlify.com
MIT License
762 stars 142 forks source link

SEO concerns #348

Closed gorkemyontem closed 6 years ago

gorkemyontem commented 6 years ago

The library works great and does the job. However, on the server side it just put an empty img tag. <img />

The reason of this expected behavior is this code:

        if (!isWindowDefined()) {
            return null;
        }

Although Google can now get to js, there are some controversies about this too. For instance: https://blog.seoprofiler.com/googles-john-mueller-lazy-loading-images-work-google/

Back in PHP time, in one of my own lazy loading implementation, I had used

What is your stand on this? I'd like to contribute with an pull request if you share my concerns.

tjoskar commented 6 years ago

Thanks for a great question!

Just to be clear, just because we return null does not mean we don't render anything, the img-tag will still be in the template.

  1. Let's say you have the following img-tag: <img [src]="defaultImage" [lazyLoad]="image" alt="my image description" title="my image title">
  2. A google search boot visits your server.
  3. The server does not have the window-object so ng-lazyload-image will leave the img-tag as it is (<img src="path-to-default-image.jpg" [lazyLoad]="image" alt="my image description" title="my image title">)
  4. The google bot gets the html and parses it. Google and other search engines do understand javascript these days but I'm not sure if they simulate a viewport so the image gets lazyloaded. I think however that it doesn't matter, as long as you have a good alt and title attribute (correct me if I am wrong).

So, I do not think SEO will be a problem as long as you:

  1. Use [src] instead of [defaultImage]
  2. Use alt and title

Or have I missed something (SEO is not my strongest suit)?

speige commented 6 years ago

+1

The image will have class "ng-failed-lazyloaded" on server-side when using AngularUniversal/SSR. I'm guessing an exception is being thrown due to window object/etc not being available in node.

I don't think it's a major SEO concern because alt/title will work. However, the actual .jpg/.png path won't be indexed. So, if you're hoping for your images to be found on images.google.com, they won't be. (or at least not on search engines that don't understand javascript). Also, a lot of social media sites will take a screenshot of the site when creating a post & they don't understand javascript.

Regardless of SEO, I think it'd be nice from a user standpoint to see the images start loading while waiting for the full angular app to load in the background. (in case of heavy download to get full angular app running due to poor AOT/treeShaking/etc)

The trick is to determine which images to load server-side. Without window object available in node & without knowing the user's screen size I don't see how you could calculate if it'll be in the initial viewport or not. It should probably be up to the consuming app programmer to understand their own layout & determine which images to eager load on SSR. Maybe something like [EagerLoadOnSSR]="true" or [EagerLoadOnSSR]="false" ?

Another option would be to have a global config to enable/disable eager load for SSR. The problem with this is that the browser will have already downloaded all images before being replaced by full angular app, so why would the full app keep lazy load enabled? I think it could still be beneficial for "virtual scrolling" apps where SSR only has 1 page of results but actual app can scroll infinitely.

speige commented 6 years ago

@gorkemyontem

For now I'm using this workaround:

import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
import {Utils} from "../services/utils";

@Directive({
    selector: '[ssrImageSrc]'
})
export class ssrImageSrcDirective implements OnChanges {
    @Input('ssrImageSrc') imageSrc: string;

    ngOnChanges(changes: Object): void {
        this.updateImageSrc();
    }

    private updateImageSrc(): void {
        if (Utils.IsAngularUniversal) {
            (<any>this.element.nativeElement).src = this.imageSrc;
        }
    }

    constructor(private element: ElementRef) {
        this.updateImageSrc();
    }
}

Utils.IsAngularUniversal is my own method to determine if I'm in SSR mode. You could swap it with Angular's built-in method isPlatformServer from @angular/common.

I'm using it like this:

<img [lazyLoad]="imageUrl" [ssrImageSrc]="imageUrl" />

It's essentially the same as this, but I assumed it'd fail because [src] and [lazyLoad] would fight each other (i didn't test it this way, so I could be wrong):

<img [lazyLoad]="imageUrl" [src]="Utils.IsAngularUniversal ? imageUrl : undefined" />
gorkemyontem commented 6 years ago

Hi @tjoskar and @speige, Thanks for the answers. I have tried both of the solutions but the result was not what I was expecting. But this is because maybe I am expecting something impossible.

Either I use [src]="defaultImage" or [src]="Utils.IsAngularUniversal ? imageUrl : undefined" it adds src on the server side. This is the behavior that I am looking for.

However, When I inspect the Network activity on Google Chrome DevTools, I see that, when the page is loaded on the server side, the images are also loaded. And whenever the client part is loaded, the images load again with the behavior of lazy loading.

Since the point of using lazy load is to reduce request number and size, doing it on server side doesn't seem like a good idea.

tjoskar commented 6 years ago

Hi @gorkemyontem,

But this is because maybe I am expecting something impossible.

What are you expecting? 😉

If the server returns src="..." the browser will start to load the image and there is nothing we can do on the client side to stop it (as far as I know). But you want to load the defaultImage, right?

Or do you mean that the defaultImage is loaded twice?

Let's say you have the following template:

<img [src]="'path-to-default-image.jpg'" [lazyLoad]="'path-to-image1.jpg'">
// some other content so you have to scroll to see the next image
<img [src]="'path-to-default-image.jpg'" [lazyLoad]="'path-to-image2.jpg'">

I expect the following to happens:

  1. The server returns the following template

    <img src="path-to-default-image.jpg" ...>
    // some other content so you have to scroll to see the next image
    <img src="path-to-default-image.jpg" ...>
  2. The browser parse the html code and loades path-to-default-image.jpg (once)

  3. ng-lazyload-image kicks in but sice you are not using [defaultImage] it will not do anything.

  4. ng-lazyload-image sets src to path-to-image1.jpg and loads it once

Is that not what is happening?

tjoskar commented 6 years ago

Closing this due to inactivity. Please let me know if the problem still occurs.