9

I have an iframe that loads a third party widget. I only want to display this iframe after my page has loaded, because I don't want to slow down my page load speed. I followed a medium article which describes how to do this, but their solution doesn't work because the onload function, finishLoading, is never called

export default ({src, width, height}) => {

  const [loading, stillLoading] = useState(true)
  const finishLoading = () => {
      alert('finished loading')
      stillLoading(false)
  }
  ...
  return(
    {loading ? '' : 
      <iframe
        src={src}
        width={width}
        height={height}
        scrolling="no"
        onLoad={finishLoading}
      >
        className={`tpwidget flex-center-row`}>
      </iframe>
    }
  )
}

Update

By using useEffect, I can get the iframe to load after everything else(theoretically) but I find that removing the iframe completely improves my PageSpeed score, and that just loading the iframe after(using useEffect) doesn't have much of a positive effect on PageSpeed.


If it helps, the domain is suddenlysask.com and the third party widget is the amazon ads.

gnujoow
  • 374
  • 1
  • 3
  • 16
Sam
  • 1,765
  • 11
  • 82
  • 176
  • From where do you get your source for the IFrame `src={src}`? Because, there is no `src` prop or state in the code – Rostyslav Jul 13 '20 at 07:48
  • @Zen_Web I'm confused by the second part of your sentence – Sam Jul 13 '20 at 07:55
  • I checked your website speed using https://www.webpagetest.org/result/200725_9B_475e21a5ad8ef0366d10fe6e42d4a5f8/ and it has an A First Byte Time so your page speed is perfect and you have only a few problems with caching static content. I checked it also with lighthouse https://lighthouse-dot-webdotdevsite.appspot.com//lh/html?url=https%3A%2F%2Fsuddenlysask.com%2F and you have a normal performance problem because of the amount of JavaScript fetched and its execution time. Check the two reports and try to fix as much as you can and you may need code-splitting or not. – Ahmed Mokhtar Jul 25 '20 at 14:35
  • For example, the Lighthouse report says that this script has https://ws-na.assoc-amazon.com/widgets/cm?o=15&p=48&l=ur1&category=summeroutlet&banner=1CZS1W6TE0KE705RZ9R2&f=ifr&linkID=2e2baca662cec7fd705d5e09af1e37a7&t=suddenlysas06-20&tracking_id=suddenlysas06-20 unused JavaScript but this script is fetched by the amazon iframe and it would be fetched anyway sooner or later. – Ahmed Mokhtar Jul 25 '20 at 14:39
  • @AhmedMokhtar There was an answer on this question that looked really useful but I hadn't gotten around to testing it out yet. The answer talked about code splitting, was it yours? If it was and you deleted it, could you repost it? – Sam Jul 25 '20 at 20:00
  • @Sam Yes it was mine and I would repost it. If you will use code-splitting you would need to figure out what you need to split. Whatever you would split it won't be included in the initial bundle and it would be fetched when the app needs it. – Ahmed Mokhtar Jul 26 '20 at 05:34
  • @Sam check my answer and more importantly the comments below it. – Ahmed Mokhtar Jul 26 '20 at 05:58

6 Answers6

7

Update

I visited the website and I'm sure you are using Gatsby. Gatsby is using SSR and React.lazy and Suspense are not yet available for server-side rendering. If you want to do code-splitting in a server-rendered app, Loadable Components is recommended in React docs. It has a nice guide for bundle splitting with server-side rendering.

There is a gatsby plugin to make your life easier gatsby-plugin-loadable-components-ssr. After you install and configure the plugin you can use loadable like this:

AmazonFrame.js

import React from "react";

const AmazonFrame = ({ src, width, height }) => (
  <iframe src={src} width={width} height={height} scrolling="no"></iframe>
);

App.js

import React from "react";
import loadable from "@loadable/component";

const AmazonFrame = loadable(() => import("./AmazonFrame"), {
  fallback: <div>Loading...</div>
});

function App() {
  return (
    <div>
      <AmazonFrame src="src" width="100%" height="200px" />
    </div>
  );
}

export default App;

or

import React from "react";
import loadable from "@loadable/component";

const AmazonFrame = loadable(() => import("./AmazonFrame"));

function App() {
  return (
    <div>
      <AmazonFrame fallback={<div>Loading...</div>} />
    </div>
  );
}

export default App;

Original answer

You need to use Code-Splitting. Code-Splitting is a feature supported by bundlers like Webpack, Rollup, and Browserify (via factor-bundle) which can create multiple bundles that can be dynamically loaded at runtime.

If you’re using Create React App, this is already configured for you and you can start using it immediately.

Code-splitting your app can help you “lazy-load” just the things that are currently needed by the user, which can dramatically improve the performance of your app. While you haven’t reduced the overall amount of code in your app, you’ve avoided loading code that the user may never need, and reduced the amount of code needed during the initial load.

Here is an example solution to your problem which will lazy load the Amazon ads iframe so it won't be loaded with your initial bundle:

AmazonFrame.js

import React from "react";

const AmazonFrame = ({ src, width, height }) => (
  <iframe src={src} width={width} height={height} scrolling="no"></iframe>
);

export default AmazonFrame;

App.js

import React, { Suspense, lazy } from "react";

// React.lazy takes a function that must call a dynamic import(). This must return a Promise
// which resolves to a module with a default export containing a React component.
const AmazonFrame = lazy(() => import("./AmazonFrame"));
function App() {
  return (
    <div>
      {/* The lazy component should then be rendered inside a Suspense component, which allows us to show some fallback
       content (such as a loading indicator) while we’re waiting for the lazy component to load */}
      {/* The fallback prop accepts any React elements that you want to render while waiting for the component to load */}
      <Suspense fallback={<div>Loading...</div>}>
        <AmazonFrame src="src" width="100%" height="200px" />
      </Suspense>
    </div>
  );
}

export default App;
Ahmed Mokhtar
  • 2,388
  • 1
  • 9
  • 19
  • Notice that the `iframe` itself isn't the problem it's just a single element. The problem is in what is fetched by the page embedded in the `iframe`. Lighthouse's main complaints are about 2 files https://d33wubrfki0l68.cloudfront.net/bundles/8ae87888184e006ea674f2b6393557e56a54c45a.js which I think is your main bundle so maybe you would need to code-split your pages so that only the page the user asks for is included in the initial bundle. – Ahmed Mokhtar Jul 26 '20 at 05:51
  • The second file is https://ws-na.assoc-amazon.com/widgets/cm?o=15&p=48&l=ur1&category=summeroutlet&banner=1CZS1W6TE0KE705RZ9R2&f=ifr&linkID=2e2baca662cec7fd705d5e09af1e37a7&t=suddenlysas06-20&tracking_id=suddenlysas06-20 which is fetched by the page embedded in the amazon ads `iframe`. I think your best option is to defer rendering this `iframe` until it's on-screen using https://usehooks.com/useOnScreen/ as @Oliver Heward suggests. – Ahmed Mokhtar Jul 26 '20 at 05:56
4

Google PageSpeed/Lighthouse doesn't just analyze the static content of the site as it is sent from the server when evaluating performance. Since most of the techniques demonstrated here load the iframe as quickly as possible (i.e. as soon as the useEffect hook is called), you wind up incurring the CPU and network request during the page-speed measurement time period.

Here are a couple of ways you can reduce the impact this iframe will have on your score. It can be hard to say what the impact will be in advance since it is dependent on the connection/network and the server performance—you'll want to test and experiment to get the best results.

  1. Use loading="lazy" on the iframe. This will instruct the browser not to load it until it gets near the viewport (e.g. if it is below the fold it won't load at all until the user scrolls down).
  2. Set a timeout in useEffect to delay loading of the iframe by a fixed duration, reducing the likelihood that it will occur during your page-speed measurement window. E.g.:
    const [ready, setReady] = useState(false)
    useEffect(() => { setTimeout(() => { setReady(true) }, 3000) }, [])
    return (<iframe src={ready ? "/your/url" : "about:blank"} loading="lazy" />)
    
  3. Similar to #2, only listen for an interaction event (e.g. scroll or click) on the page and only load the ads once that event has occurred.
coreyward
  • 77,547
  • 20
  • 137
  • 166
  • Property 'loading' does not exist on type 'DetailedHTMLProps, HTMLIFrameElement>'.ts(2322) – Sam Jul 29 '20 at 09:45
  • @Sam I'm not sure what you need to do to make TypeScript aware of it, but you can see from both [caniuse](https://caniuse.com/#feat=loading-lazy-attr) and [MDN](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#Images_and_iframes) that it exists and has browser support. – coreyward Jul 29 '20 at 17:39
  • 2
    I dove in and found that DefinitelyTyped doesn't have a definition for this standard so I've [opened a pull request to add it](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/46434). – coreyward Jul 29 '20 at 17:56
2

The iframe never loads!

React will not mount the iframe in the first place since it is mounted conditionally (loading) and the condition never changes since it's dependent on the iframe being mounted.

You can load the iframe outside react and attach it in a useEffect hook:

export default ({src, width, height}) => {

    const container = useRef()
    useEffect(() => {
        const frm = document.createElement('iframe')
        frm.src = src
        frm.width = width
        frm.height = height
        frm.scrolling = 'no'
        container.current.appendChild(frm)
    }, [])

    return(
        <div ref={container} />
    )
}

This being said, I can't tell you if this will speed up page loading, after all, iframes load external content. But you can give it a shot.

Mordechai
  • 15,437
  • 2
  • 41
  • 82
  • Yes the solution by AskMen pointed out that I can use useEffect to load the iframe after the page load, but I find that I get better page speeds if I just remove the iFrame alltogether – Sam Jul 23 '20 at 02:51
  • No, AskMen's solution doesn't load the iframe in the effect, it just marks (falsely) that loading is complete and then actually starts the load on render. – Mordechai Jul 23 '20 at 03:56
  • 1
    You'll end up with better load speeds because an iFrame embeds another webpage into the website. This means that it needs to grab the content and embed it which will always affect load speed. Loading the iFrame after the component has mounted with the useEffect hook is probably the best work around. Although if you don't need to deliver the iFrame on pageload (if it doesn't sit in the viewport) you could always try defer it offscreen and grab the useOnScreen custom hook to load it when it becomes visible in the viewport. Although I'm not 100% sure this will help assist the load speed. – Oliver Heward Jul 23 '20 at 12:10
  • 1
    @OliverHeward You should post a solution – Sam Jul 29 '20 at 22:01
  • I would but I am sure there are some much better answers that have been posted since this was asked :) – Oliver Heward Jul 30 '20 at 14:51
1

There is a new attribute added lately which improves the perfomance of iframe. It's called loading and it can have 2 values indicating how the browser should load the iframe: eager (load the iframe immediately) and lazy (Defer loading of the iframe until it reaches a calculated distance from the viewport). So, it will be something like: <iframe src={src} loading="eager"/> Read more about it here

Donald Shahini
  • 835
  • 9
  • 20
0

You have to use useEffect() hook.

useEffect(() => {
  stillLoading(false)
}, [])

In this case stillLoading(false) will be applied only after the component is mounted. So, at the end your condition will work after loading the page.

Asking
  • 3,487
  • 11
  • 51
  • 106
  • Right, I don't know why I didn't think of this – Sam Jul 13 '20 at 08:01
  • So I did this, but my third party iframe is still really reducing my page load time. I might just have to get rid of the iframe because that one ad is giving me warnings on page speed insights for text compression, unused javascript, properly sized images, efficiently encoded images and a few others. Maybe these warnings will happen regardless? I think my pagespeed rating goes up 20 points when I remove the ad, but if there's a way I can keep it and have it not affect my pagespeed score I'd like to do that – Sam Jul 17 '20 at 21:11
  • 1
    This just renders and loads the iframe on the next render cycle. It won't load it in the background. – Mordechai Jul 22 '20 at 21:15
  • This is the right answer because the application is server-side rendered by gatsby and the `useEffect` won't run on initial render when gatsby builds the pages so it will prevent the `iframe` from being rendered. – Ahmed Mokhtar Jul 25 '20 at 13:09
0

You can use window.onload function to load iframe during runtime.

function loadDeferredIframe() {
    // this function will load the Google homepage into the iframe
    var iframe = document.getElementById("my-deferred-iframe");
    iframe.src = "./" // here goes your url
};
window.onload = loadDeferredIframe;

At first you can have a iframe element pointing to blank and can change the src during runtime.

<iframe id="my-deferred-iframe" src="about:blank" />
Divyesh Puri
  • 196
  • 8