Etiqueta: DataTables

  • Por qué tus páginas con DataTables tardan tanto en cargar

    Por qué tus páginas con DataTables tardan tanto en cargar

    Una página que tarda mucho en cargar es una página mala.

    Lo sabés vos y lo saben tus usuarios.

    Si tu sitio tarda más de 5 segundos en mostrarse tus visitantes se sentirán así:

    Las páginas que usan DataTables se ven bien y son muy funcionales pero pueden tardar una eternidad en desplegarse.

    Más de la mitad de los usuarios que no encuentran pronto lo que buscan se van a otro sitio.

    Y los que se quedan desconfían de la profesionalidad de quien las desarrolló.

    ¿Y quién quiere contratar a alguien que entrega trabajos de dudosa calidad?

    Más vale dedicar unos minutos a aprender cómo mejorarlas.

    Factores que afectan el tiempo de carga de tu página

    En su forma más simple, un DataTable puede cargarse usando un arreglo JavaScript.

    Basta un código como este:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="//cdn.datatables.net/1.10.24/css/jquery.dataTables.min.css">
        <title>DataTables example</title>
    </head>
    <body>
    <h1>Behold... the power of DataTables!</h1>
    <table id="theTable" class="display" style="width: 100%">
        <thead>
            <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Price</th>
            </tr>
        </thead>
        <tfoot>
            <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Price</th>
            </tr>
        </tfoot>
    </table>
    </body>
    </html>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
    <script src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script>
    <script type="application/javascript">
        $(document).ready( function () {
            $('#theTable').DataTable({
                'data': [
                    [1, 'Box', 10.0],
                    [2, 'Chair', 20.2]
                ],
            });
        } );
    </script>

    Por supuesto que si toda la información tuviese que estar hardcodeada no sería muy útil la aplicación, ¿cierto?

    Una versión algo más realista sería combinar este código con algo de PHP que levante información de una db, por ejemplo:

    <?php
    
    try {
        $conn = new PDO('sqlite:dt.sq3');
    } catch (PDOException $exception) {
        die($exception->getMessage());
    }
    
    $sql = "SELECT * FROM products";
    $st = $conn
    ->query($sql);
    
    if ($st) {
    $rs = $st->fetchAll(PDO::FETCH_ASSOC );
    ?>
    <script type="application/javascript">
        $(document).ready( function () {
            $('#theTable').DataTable({
                'data': [
                    <?php
                    foreach ($rs as $row) {
                        ?>
                    [ '<?php echo $row['id'];?>', '<?php echo $row['name'];?>', '<?php echo $row['price'];?>' ],
                        <?php
                    }
                    ?>
                ],
            });
        } );
    </script>
    <?php
    } else {
        var_dump($conn->errorInfo());
        die;
    }

    Hasta aquí, todo perfecto.

    El problema se presenta cuando la cantidad de registros es grande.

    La definición de grande es un poco esquiva… podés empezar a notar problemas con 500 registros pero si estás tratando con algo como 150 000 o más definitivamente tus páginas van a tardar una eternidad en mostrarse.

    Aún si tenés páginas pequeñas (Digamos, de 10 registros cada una), el tiempo total de carga no se modifica en absoluto…

    ¿Por qué sucede esto?

    Como en cualquier otro script, esto es, a grosso modo lo que ocurre:

    1. El servidor recibe la petición
    2. Se ejecuta el PHP
    3. Se realiza la consulta a la db
    4. El servidor recibe todos los registros de la tabla
    5. Se genera el HTML y el JavaScript
    6. Se envía todo al cliente
    7. El cliente interpreta el HTML y el JavaScript
    8. Se ejecuta el JavaScript y se dibuja la tabla y todo lo demás

    Como te podrás imaginar, para trabajar con los registros de la primera página no es realmente necesario tener en memoria las otras 500…

    Toda esa información que no se utilizará no hace más que sobrecargar tanto al servidor (especialmente al de bases de datos) como al cliente.

    Es más, si hay muchos usuarios accediendo a la vez el problema se va multiplicando, con lo cual la experiencia de uso es peor para todos 🙁

    Podrías intentar atacar el problema mejorar los recursos de hardware, implementando algún tipo de caché o similar y es posible que todo eso ayudara.

    Sin embargo, existe una solución mucho más al alcance de tus manos.

    La solución: Procesamiento del lado del Servidor

    Y aquí viene la genialidad de DataTables… ¡lo pensaron todo estos muchachos!

    Este plugin admite dos modos de procesamiento: Client-Side (Es decir, todo se hace en el navegador del cliente) o Server-Side (El procesamiento se hace del lado del servidor y sólo se realiza el rendering del lado del cliente).

    El modo por defecto es, por supuesto, Client-Side.

    He ahí la clave del problema.

    Trabajar con Server-Side es un poco más complicado, pero tampoco es terrible.

    Lo más importante es comprender que, en este modo, DataTables hará una llamada Ajax cada vez que se requiera re-dibujar la información de la tabla (Al paginar, ordenar, etc…).

    Para comenzar a usar este modo necesitás cambiar la configuración de tu DataTable por algo como:

    $('#theTable').DataTable( {
        serverSide: true,
        ajax: '/get_data.php'
    } );

    Claro que, a diferencia del ejemplo en que la tabla se carga por Ajax sólo al comienzo (o cuando se refresca), esta versión de get_data.php deberá ser bastante más inteligente para poder responder a las órdenes que le dará el FrontEnd.

    Qué envía el FrontEnd al BackEnd en DataTables Server-Side

    La comunicación entre FrontEnd y BackEnd se realizará a través del envío de parámetros a través de la URL de la llamada (salvo que lo cambies explícitamente).

    Esto implica que tu script php deberá recogerlos desde $_GET.

    A modo de resumen te comento los más importantes (La lista completa la podés encontrar acá):

    start: Número de registro por el que debe comenzar la paginación

    length: Cantidad de registros que se espera que devuelva el servidor. Es decir, a partir del registro start, retorná length registros (Similar al LIMIT X OFFSET Y de SQL)

    search: Arreglo que contiene los parámetros de la búsqueda realizada por el usuario. Esta búsqueda es global, es decir, aplica a todas aquellas columnas marcadas como searchable.

    • value: El texto ingresado por el usuario en el cuadro de búsqueda
    • regex: Booleano que indica si el value debe ser interpretado en forma literal o como expresión regular

    columns: Arreglo con una entrada por cada columna de la tabla.

    Cada entrada de este arreglo será, a su vez, un arreglo con las siguientes claves:

    • data: información de mapeo entre el arreglo de datos y la tabla HTML (En este ejemplo se trata del índice que vincula la columna en la visualización con el campo en la consulta: id => 0, name => 1, price => 2)
    • name: Nombre de la columna como está definido en la propiedad columns.name (En este ejemplo no lo estamos usando así que será un string vacío).
    • searchable: Booleano que indica si es posible realizar búsquedas sobre esta columna
    • orderable: Booleano que indica si es posible realizar ordenamientos basados en esta columna
    • search: Arreglo que contiene los parámetros de la búsqueda realizada por el usuario. Similar a la búsqueda global, sólo que aplicada a esta columna en particular (Un caso algo más avanzado y no muy frecuentemente utilizado).
      • value: El texto ingresado por el usuario en el cuadro de búsqueda
      • regex: Booleano que indica si el value debe ser interpretado en forma literal o como expresión regular

    order: Arreglo que especifica el criterio de ordenamiento que debe aplicarse, contendrá una entrada por cada criterio a utilizar (Para el caso de que se quieran combinar varios). Dentro de cada entrada contendrá un arreglo con dos claves:

    1. column: Número de columna por la que se quiere ordenar
    2. dir: String con la dirección del ordenamiento (asc para ascendente, desc para descendiente)

    Como te podrás imaginar, la URL de la llamada puede ser algo extensa 🙂

    ¿No me creés? ¿Qué te parece esta?

    http://localhost:8000/get_data.php?draw=1&columns%5B0%5D%5Bdata%5D=0&columns%5B0%5D%5Bname%5D=&columns%5B0%5D%5Bsearchable%5D=true&columns%5B0%5D%5Borderable%5D=true&columns%5B0%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B0%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B1%5D%5Bdata%5D=1&columns%5B1%5D%5Bname%5D=&columns%5B1%5D%5Bsearchable%5D=true&columns%5B1%5D%5Borderable%5D=true&columns%5B1%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B1%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B2%5D%5Bdata%5D=2&columns%5B2%5D%5Bname%5D=&columns%5B2%5D%5Bsearchable%5D=true&columns%5B2%5D%5Borderable%5D=true&columns%5B2%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B2%5D%5Bsearch%5D%5Bregex%5D=false&order%5B0%5D%5Bcolumn%5D=0&order%5B0%5D%5Bdir%5D=asc&start=0&length=10&search%5Bvalue%5D=&search%5Bregex%5D=false&_=1619613946473

    Si la pasás a través de urldecode queda un poquito más entendible:

    http://localhost:8000/get_data.php?draw=1&columns[0][data]=0&columns[0][name]=&columns[0][searchable]=true&columns[0][orderable]=true&columns[0][search][value]=&columns[0][search][regex]=false&columns[1][data]=1&columns[1][name]=&columns[1][searchable]=true&columns[1][orderable]=true&columns[1][search][value]=&columns[1][search][regex]=false&columns[2][data]=2&columns[2][name]=&columns[2][searchable]=true&columns[2][orderable]=true&columns[2][search][value]=&columns[2][search][regex]=false&order[0][column]=0&order[0][dir]=asc&start=0&length=10&search[value]=&search[regex]=false&_=1619613946473

    O, todavía mejor si lo ves desde la consola del navegador:

    De modo que si del lado de tu PHP hacés un var_dump($_GET) te vas a encontrar con:

    array (size=7)
      'draw' => string '1' (length=1)
      'columns' => 
        array (size=3)
          0 => 
            array (size=5)
              'data' => string '0' (length=1)
              'name' => string '' (length=0)
              'searchable' => string 'true' (length=4)
              'orderable' => string 'true' (length=4)
              'search' => 
                array (size=2)
                  ...
          1 => 
            array (size=5)
              'data' => string '1' (length=1)
              'name' => string '' (length=0)
              'searchable' => string 'true' (length=4)
              'orderable' => string 'true' (length=4)
              'search' => 
                array (size=2)
                  ...
          2 => 
            array (size=5)
              'data' => string '2' (length=1)
              'name' => string '' (length=0)
              'searchable' => string 'true' (length=4)
              'orderable' => string 'true' (length=4)
              'search' => 
                array (size=2)
                  ...
      'order' => 
        array (size=1)
          0 => 
            array (size=2)
              'column' => string '0' (length=1)
              'dir' => string 'asc' (length=3)
      'start' => string '0' (length=1)
      'length' => string '10' (length=2)
      'search' => 
        array (size=2)
          'value' => string '' (length=0)
          'regex' => string 'false' (length=5)
      '_' => string '1619615674442' (length=13)

    Y de ahí… bueno, habrá que hacer la consulta que corresponda, por ejemplo:

    <?php
    
    try {
        $conn = new PDO('sqlite:dt.sq3');
    } catch (PDOException $exception) {
        die($exception->getMessage());
    }
    
    $orderBy = " ORDER BY ";
    foreach ($_GET['order'] as $order) {
        $orderBy .= $order['column'] + 1 . " {$order['dir']}, ";
    }
    
    $orderBy = substr($orderBy, 0, -2);
    $where = '';
    
    $columns = $_GET['columns'];
    $fields = ['id', 'name', 'price'];
    $where = '';
    
    foreach ($columns as $k => $column) {
        if ($search = $column['search']['value']) {
            $where .= $fields[$k].' = '.$search.' AND ';
        }
    }
    
    $where = substr($where, 0, -5);
    $length = $_GET['length'];
    $start = $_GET['start'];
    
    $sql = "SELECT * FROM products ".($where ? "WHERE $where " : '')."$orderBy LIMIT $length OFFSET $start";

    (Me tomé una pequeña licencia… asumí que el search no será una expresión regular… te queda como ejercicio :))

    Cómo debe responder el BackEnd al FrontEnd en DataTables Server-Side

    Por otro lado, es importante que tu php sepa qué es lo que DataTables está esperando como respuesta, de otro modo la comunicación no podrá realizarse exitosamente.

    Lo principal: DataTables espera un resultado JSON que debe contener:

    data: El arreglo de los registros que se levantaron de la base

    recordsTotal: Número total de registros que existen en la base (Sin aplicar filtros)

    recordsFiltered: Número de registros que devuelve la consulta (Una vez aplicados los filtros).

    En definitiva, el código php se completa con:

    $countSql = "SELECT count(id) as Total FROM products";
    $countSt = $conn
        ->query($countSql);
    
    $total = $countSt->fetch()['Total'];
    
    $sql = "SELECT * FROM products ".($where ?? "WHERE $where ")."$orderBy LIMIT $length OFFSET $start";
    $st = $conn
        ->query($sql);
    
    if ($st) {
        $rs = $st->fetchAll(PDO::FETCH_FUNC, fn($id, $name, $price) => [$id, $name, $price] );
    
        echo json_encode([
            'data' => $rs,
            'recordsTotal' => $total,
            'recordsFiltered' => count($rs),
        ]);
    } else {
        var_dump($conn->errorInfo());
        die;
    }

    El código completo lo podés descargar de aquí.

    DataTables ServerSide FTW!

    En este artículo aprendiste las razones por las que tu página con DataTables tarda mucho en cargar y cómo podés acelerar los tiempos de carga.

    ¿Siempre te conviene usar ServerSide? No

    Si la cantidad de registros es pequeña (O más bien diría si no hay retrasos visibles) no te conviene hacer una implementación con esta complejidad, pero en cambio, si los DataTables se están volviendo un cuello de botella SeverSide puede ser una buena solución.

    Ahora que lo sabés, andá a acelerar esa página que los usuarios están esperando!

  • Construyendo una tabla dinámica con PHP, MySQL, DataTables y Ajax

    Construyendo una tabla dinámica con PHP, MySQL, DataTables y Ajax

    ¿Cuántas veces te enfrentaste a la necesidad de mostrar información resumida en forma de tablas?

    O, puesto de otro modo: ¿qué aplicación no requiere del uso de tablas?

    Por supuesto que se puede usar HTML puro y quedará algo más o menos aceptable… pero con las herramientas (¡y los usuarios!) que tenemos hoy en día no podemos darnos el lujo de presentar una experiencia de usuario aceptable.

    Debemos dar lo mejor que podamos.

    Y lo mejor hoy en día implica interactividad.

    Los usuarios quieren poder buscar, ordenar, paginar… prácticamente tener un Excel directo en su aplicación.

    Ya te estarás imaginando el delirio de javascript que tendrías que ponerte a escribir para hacer todo esto desde cero, ¿no? De sólo pensarlo me dan ganas de retomar el curso de peluquería que dejé a la mitad 🙂

    Afortunadamente no es necesario re-inventar la rueda.

    Como podrás imaginarte, existen ya varios componentes que pueden adquirirse gratis (o a muy bajo costo) y que te ahorrarán muchísimas horas de trabajo y dolores de cabeza.

    DataTables es un plugin de jQuery que resuelve muy bien esta necesidad, desafortunadamente la documentación es algo compleja de seguir…

    Es por eso que me propuse escribir este artículo donde te presento un ejemplo completo de implementación de una tabla dinámica que utiliza DataTables combinado con un script php que actúa a modo de WebService REST para levantar los datos de una base.

    Cómo se ve un DataTable

    Empecemos por lo más importante: ¿sirve un DataTable para resolver tu necesidad?

    Una imagen vale más que mil palabras, así que te muestro el ejemplo de una tabla en la que se visualiza el inventario de un comercio: una tabla en la que se verán los datos de cada uno de los productos (Id, Nombre, Precio):

    No está mal, ¿no?

    Repasemos lo que tenemos:

    • ¿Paginación? Lo tenemos!
    • ¿Búsqueda? Lo tenemos!
    • ¿Ordenamiento? Lo tenemos!

    ¿Qué más se le puede pedir a la vida? 🙂

    Vamos ahora a ver qué hay detrás de la cortina.

    El FrontEnd

    El frontend consta de un archivo HTML que se ve así:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="//cdn.datatables.net/1.10.24/css/jquery.dataTables.min.css">
        <title>DataTables example</title>
    </head>
    <body>
    <h1>Behold... the power of DataTables!</h1>
    <table id="theTable" class="display" style="width: 100%">
        <thead>
            <tr>
                <th>id</th>
                <th>name</th>
                <th>price</th>
            </tr>
        </thead>
        <tfoot>
            <tr>
                <th>id</th>
                <th>name</th>
                <th>price</th>
            </tr>
        </tfoot>
    </table>
    </body>
    </html>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
    <script src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script>
    <script type="application/javascript">
        $(document).ready( function () {
            $('#theTable').DataTable({
                ajax: '/get_data.php',
            });
        } );
    </script>

    Analicémoslo un poco:

    <link rel="stylesheet" href="//cdn.datatables.net/1.10.24/css/jquery.dataTables.min.css">

    Esta parte incluye la hoja que aporta los estilos que necesita DataTables.

    La parte principal es la tabla:

    <table id="theTable" class="display" style="width: 100%">
        <thead>
            <tr>
                <th>id</th>
                <th>name</th>
                <th>price</th>
            </tr>
        </thead>
        <tfoot>
            <tr>
                <th>id</th>
                <th>name</th>
                <th>price</th>
            </tr>
        </tfoot>
    </table>

    Es importante que la tabla esté bien formada (Que tenga su sección thead, el tfoot es opcional).

    No hace falta que le pongas un tbody, lo hace todo el plugin.

    Luego tenemos el código JavaScript:

    <script type="application/javascript">
        $(document).ready( function () {
            $('#theTable').DataTable({
                ajax: '/get_data.php'
            });
        } );
    </script>

    Es muy sencillito, se trata de invocar una función cuando el documento está cargado por completo.

    Usando jQuery eso se logra con:

    $(document).ready();

    Y lo que ves dentro es el callback que se invocará cuando este evento ocurra.

    $('#theTable')

    Es la forma de seleccionar el objeto cuyo id es theTable.

    En este caso se trata, obviamente, de la tabla que definimos en el HTML.

    Y al invocar el método DataTable estamos transformando esa simple tabla en un objeto mucho más complejo.

    Por último, este método recibe un parámetro: un objeto que contiene la configuración de la nueva tabla.

    En nuestro caso se ve así:

    {
       ajax: '/get_data.php'
    }

    Lo cual significa que le estamos pasando una configuración con una única propiedad: ajax y su valor (/get_data.php) es la URL de donde se obtendrán, mediante una llamada ajax, los valores con los que se rellenará la tabla.

    El BackEnd

    Ahora que viste cómo funciona el lado cliente, demos una recorrida por la parte del php:

    <?php
    
    try {
        $conn = new PDO('mysql:host=localhost;dbname=ecommerce','root','root');
    } catch (PDOException $exception) {
        die($exception->getMessage());
    }
    
    $sql = "SELECT * FROM products";
    $st = $conn
        ->query($sql);
    
    if ($st) {
        $rs = $st->fetchAll(PDO::FETCH_FUNC, fn($id, $name, $price) => [$id, $name, $price] );
    
        echo json_encode([
            'data' => $rs,
        ]);
    } else {
        var_dump($conn->errorInfo());
        die;
    }

    En este ejemplo estoy usando PDO aunque bien podría usar MySQLi ya que se trata de una base MySQL.

    La primera parte:

    try {
        $conn = new PDO('mysql:host=localhost;dbname=ecommerce','root','root');
    } catch (PDOException $exception) {
        die($exception->getMessage());
    }

    Intenta realizar una conexión y, en caso de fallar emite un mensaje por pantalla para ayudar con el debugging.

    Con este código:

    $sql = "SELECT * FROM products";
    $st = $conn
        ->query($sql);

    Se intenta hacer la consulta de selección de los datos de los productos de la base.

    Si la consulta fue exitosa buscamos el resultado para enviarlo al FrontEnd respondiendo a la llamada Ajax:

    $rs = $st->fetchAll(PDO::FETCH_FUNC, fn($id, $name, $price) => [$id, $name, $price] );
    
    echo json_encode([
        'data' => $rs,
    ]);

    La primera línea se ve algo extraña, ¿no?

    Paso a explicar 🙂

    Un detalle que puede ahorrarte varios dolores de cabeza es comprender cómo DataTables espera recibir los resultados del webservice que le brinda los datos.

    Se trata de un objeto json con este formato:

    {
       data:[
         [ f1, f2, f3 ],
         [ f1, f2, f3 ],
         [ f1, f2, f3 ],
       ]
    }

    Donde f* corresponde a un campo del registro encontrado.

    En este caso, para que todo salga bien, el php debe responder con algo como:

    {
       data:[
         [ 1, 'Chair', 200.0 ],
         [ 2, 'Table', 500.0 ],
         [ 3, 'Shoes', 450.0 ],
       ]
    }

    Y hay que aclarar: DataTables es muy estricto con esto!

    Es por eso que usé PDO::FETCH_FUNC (Bueno, por eso y porque soy un fanático de la programación funcional) para generar, en base a los registros obtenidos, el arreglo correspondiente.

    Y luego es simple: con la función json_encode se genera el string que se requiere.

    En conclusión

    Espero que este pequeño ejemplo te haya ayudado a comprender lo que podés lograr con este plugin y un poco de php.

    La próxima vez que tengas que mostrar resultados en forma de tabla probalo y comentá cómo te fué 🙂

    Ah! Y si querés descargar el código completo podés hacerlo desde GitHub.